diff --git a/.codex-task/GLOBAL-TODO.md b/.codex-task/GLOBAL-TODO.md new file mode 100644 index 000000000..581f0124f --- /dev/null +++ b/.codex-task/GLOBAL-TODO.md @@ -0,0 +1,11 @@ +# 全局 TODO + +仅记录各 goal 完成后仍有价值、但不属于已完成 goal 的后续事项。 + +## Hubble 2.0 + +- 完成 Hubble UI/UX 重构:基于真实双模式与 Chrome 基线完成三方向选型,交付“理解图、准备数据、查询分析”三个核心旅程,并以能力映射、可访问性、性能和发布包回归收口。 +- 现代化 React、Ant Design、X6/Graphin/G6、Dagre 与构建链;先固化核心页面、图交互、性能和浏览器回归基线,再分批升级并清理兼容层。 +- HugeGraph Server 提供保留 Schema 的 data-only clear API 后,恢复 Hubble“仅清数据”能力及权限、默认图和失败合同测试。 +- 重构 Gremlin Basic 认证,移除 HTTP Session 中的短期明文凭据,并验证 token 生命周期。 +- 发布加固:建立逐 JAR license/native allowlist 与 NOTICE 审计,RC 使用正式发布密钥验证 `.sha512`/`.asc`。 diff --git a/.github/actions/get-hugegraph-commit/action.yml b/.github/actions/get-hugegraph-commit/action.yml index f6cf438d7..b73b49451 100644 --- a/.github/actions/get-hugegraph-commit/action.yml +++ b/.github/actions/get-hugegraph-commit/action.yml @@ -1,22 +1,49 @@ -name: 'Get HugeGraph stable commit id' -description: 'Fetch the latest HugeGraph release commit SHA and expose it as output and env variable' +name: 'Get HugeGraph commit id' +description: 'Fetch a HugeGraph PR or latest release commit SHA and expose it as output and env variable' + +inputs: + server-pr-number: + description: 'HugeGraph Server PR number to use as integration baseline' + required: false + default: '' outputs: commit_id: - description: 'The commit SHA of the latest HugeGraph release' + description: 'The HugeGraph commit SHA selected as integration baseline' value: ${{ steps.get-commit.outputs.commit_id }} + fetch_ref: + description: 'Fetchable HugeGraph git ref selected as integration baseline' + value: ${{ steps.get-commit.outputs.fetch_ref }} runs: using: composite steps: - name: Get HugeGraph stable commit id id: get-commit - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const owner = 'apache'; const repo = 'hugegraph'; + const serverPrNumber = '${{ inputs.server-pr-number }}'; try { + if (serverPrNumber) { + const pull_number = Number(serverPrNumber); + if (!Number.isInteger(pull_number) || pull_number <= 0) { + throw new Error(`Invalid server PR number: ${serverPrNumber}`); + } + const { data: pull } = await github.rest.pulls.get({ + owner, repo, pull_number + }); + const sha = pull.head.sha; + const fetchRef = `refs/pull/${pull_number}/head`; + core.exportVariable('COMMIT_ID', sha); + core.exportVariable('COMMIT_REF', fetchRef); + core.setOutput('commit_id', sha); + core.setOutput('fetch_ref', fetchRef); + console.log(`Using HugeGraph Server PR #${pull_number} ${fetchRef} (${sha})`); + return; + } const { data: release } = await github.rest.repos.getLatestRelease({ owner, repo }); const tagName = release.tag_name; const { data: ref } = await github.rest.git.getRef({ @@ -30,7 +57,9 @@ runs: throw new Error(`Unexpected ref type: ${ref.object.type}`); } core.exportVariable('COMMIT_ID', sha); + core.exportVariable('COMMIT_REF', `refs/tags/${tagName}`); core.setOutput('commit_id', sha); + core.setOutput('fetch_ref', `refs/tags/${tagName}`); console.log(`Using HugeGraph release ${tagName} (${sha})`); } catch (error) { core.setFailed(`Failed to get HugeGraph commit: ${error.message}`); diff --git a/.github/actions/setup-java-env/action.yml b/.github/actions/setup-java-env/action.yml index 18b136123..9d897bfd0 100644 --- a/.github/actions/setup-java-env/action.yml +++ b/.github/actions/setup-java-env/action.yml @@ -19,7 +19,7 @@ runs: using: composite steps: - name: Install JDK ${{ inputs.java-version }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: ${{ inputs.java-version }} distribution: ${{ inputs.distribution }} diff --git a/.github/actions/upload-coverage/action.yml b/.github/actions/upload-coverage/action.yml index 7b07b84f2..143da955b 100644 --- a/.github/actions/upload-coverage/action.yml +++ b/.github/actions/upload-coverage/action.yml @@ -14,7 +14,7 @@ runs: using: composite steps: - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v7 with: token: ${{ inputs.token }} - file: ${{ inputs.file }} + files: ${{ inputs.file }} diff --git a/.github/labeler.yml b/.github/labeler.yml index e1ee55ba0..62bb14990 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -18,16 +18,19 @@ # # Pull Request Labeler Github Action Configuration: https://github.com/marketplace/actions/labeler -name: Add Scope Label - client: - - hugegraph-client*/**/* -# - hugegraph-client-go/**/* + - changed-files: + - any-glob-to-any-file: 'hugegraph-client*/**/*' +# - any-glob-to-any-file: 'hugegraph-client-go/**/*' hubble: - - hugegraph-hubble/**/* + - changed-files: + - any-glob-to-any-file: 'hugegraph-hubble/**/*' loader: - - hugegraph-loader/**/* + - changed-files: + - any-glob-to-any-file: 'hugegraph-loader/**/*' tools: - - hugegraph-tools/**/* + - changed-files: + - any-glob-to-any-file: 'hugegraph-tools/**/*' spark: - - hugegraph-spark-connector/**/* + - changed-files: + - any-glob-to-any-file: 'hugegraph-spark-connector/**/*' diff --git a/.github/workflows/client-ci.yml b/.github/workflows/client-ci.yml index 9ed4e723f..e076dd256 100644 --- a/.github/workflows/client-ci.yml +++ b/.github/workflows/client-ci.yml @@ -5,7 +5,7 @@ on: push: branches: - master - - /^release-.*$/ + - 'release-*' paths: - hugegraph-client/** - hugegraph-dist/** @@ -32,7 +32,7 @@ jobs: steps: - name: Fetch code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 2 diff --git a/.github/workflows/client-go-ci.yml b/.github/workflows/client-go-ci.yml index 9c14711e9..520d92d95 100644 --- a/.github/workflows/client-go-ci.yml +++ b/.github/workflows/client-go-ci.yml @@ -4,7 +4,7 @@ on: push: branches: - master - - /^release-.*$/ + - 'release-*' paths: - hugegraph-client-go/** - hugegraph-dist/** @@ -30,7 +30,7 @@ jobs: steps: - name: Fetch Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 2 @@ -52,9 +52,10 @@ jobs: commit-id: ${{ steps.get-commit.outputs.commit_id }} - name: Init Go env - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: '1.x' + cache-dependency-path: hugegraph-client-go/go.sum - name: Go test run: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 314b54df7..83c3e4d9d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,10 +38,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 - name: Setup Java JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: '11' @@ -53,12 +53,12 @@ jobs: mv -vf .github/configs/settings.xml $HOME/.m2/settings.xml - name: Use Node.js 16 - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: node-version: '16' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -73,7 +73,7 @@ jobs: # If this step fails, then you should remove it and run the build manually (see below) - if: matrix.language == 'python' || matrix.language == 'javascript' name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v4 - if: matrix.language == 'java' name: Build Java @@ -91,12 +91,12 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 dependency-review: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v7 - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 + uses: actions/dependency-review-action@v5.0.0 diff --git a/.github/workflows/hubble-ci.yml b/.github/workflows/hubble-ci.yml index 2b09e74ff..5445dbb0b 100644 --- a/.github/workflows/hubble-ci.yml +++ b/.github/workflows/hubble-ci.yml @@ -5,7 +5,7 @@ on: push: branches: - master - - /^release-.*$/ + - 'release-*' paths: - hugegraph-hubble/** - hugegraph-dist/** @@ -23,6 +23,8 @@ on: env: TRAVIS_DIR: hugegraph-hubble/hubble-dist/assembly/travis + HUGEGRAPH_SERVER_COMMIT: 99936be5f41fccd193f120e01206e3cf3c73a050 + HUGEGRAPH_SERVER_FETCH_REF: refs/heads/master jobs: hubble-ci: @@ -37,27 +39,74 @@ jobs: steps: - name: Fetch Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 2 - - name: Get HugeGraph stable commit id - id: get-commit - uses: ./.github/actions/get-hugegraph-commit - - name: Setup Java environment uses: ./.github/actions/setup-java-env with: java-version: ${{ matrix.JAVA_VERSION }} - distribution: 'adopt' + distribution: 'temurin' use-stage: ${{ env.USE_STAGE }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: 'pip' + - name: Set up Node.js 18.20.8 + uses: actions/setup-node@v6 + with: + node-version: '18.20.8' + + # we also should cache python & yarn & downloads to avoid useless work + - name: Cache Maven packages + uses: actions/cache@v6 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Cache Node modules + uses: actions/cache@v6 + with: + path: | + hugegraph-hubble/hubble-fe/node_modules + hugegraph-hubble/hubble-fe/node + ~/.cache/yarn + key: ${{ runner.os }}-node-${{ hashFiles('**/package.json', '**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Cache Playwright browsers + uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('hugegraph-hubble/hubble-fe/yarn.lock') }} + + - name: Cache HugeGraph Server + uses: actions/cache@v6 + with: + path: ~/hugegraph-cache-${{ env.HUGEGRAPH_SERVER_COMMIT }} + key: ${{ runner.os }}-hugegraph-server-${{ env.HUGEGRAPH_SERVER_COMMIT }} + + - name: Frontend i18n check + working-directory: hugegraph-hubble/hubble-fe + env: + NODE_OPTIONS: --max-old-space-size=4096 + run: | + yarn install --frozen-lockfile --network-timeout 600000 --prefer-offline --ignore-engines + yarn lint + yarn i18n:check + node --test ../hubble-dist/assembly/travis/ui_auth.test.js + + - name: Install Playwright Chromium + working-directory: hugegraph-hubble/hubble-fe + run: yarn playwright install --with-deps chromium + - name: Compile run: | mvn install -pl hugegraph-client,hugegraph-loader -am -Dmaven.javadoc.skip=true -DskipTests -ntp @@ -66,8 +115,19 @@ jobs: - name: Prepare env and service env: - COMMIT_ID: ${{ steps.get-commit.outputs.commit_id }} + COMMIT_ID: ${{ env.HUGEGRAPH_SERVER_COMMIT }} + COMMIT_REF: ${{ env.HUGEGRAPH_SERVER_FETCH_REF }} + NODE_OPTIONS: --max-old-space-size=4096 run: | + echo "=== Environment Info ===" + node -v + yarn -v + echo "NODE_OPTIONS: $NODE_OPTIONS" + echo "COMMIT_ID: $COMMIT_ID" + echo "COMMIT_REF: $COMMIT_REF" + free -h || true + echo "=======================" + python -m pip install -r ${TRAVIS_DIR}/requirements.txt cd hugegraph-hubble mvn package -Dmaven.test.skip=true @@ -77,17 +137,61 @@ jobs: ./stop-hubble.sh cd ../../../ pwd - $TRAVIS_DIR/install-hugegraph.sh $COMMIT_ID + $TRAVIS_DIR/install-hugegraph.sh $COMMIT_ID $COMMIT_REF + + - name: Release package audit + env: + GNUPGHOME: ${{ runner.temp }}/hubble-release-gate-gpghome + run: | + HUBBLE_TARBALL="$(ls hugegraph-hubble/target/apache-hugegraph-hubble-*.tar.gz | head -n 1)" + if [[ -z "${HUBBLE_TARBALL}" ]]; then + echo "Hubble tarball not found" >&2 + exit 1 + fi + + mkdir -p "${GNUPGHOME}" + chmod 700 "${GNUPGHOME}" + shasum -a 512 "${HUBBLE_TARBALL}" > "${HUBBLE_TARBALL}.sha512" + printf '%s\n' \ + '%no-protection' \ + 'Key-Type: RSA' \ + 'Key-Length: 2048' \ + 'Subkey-Type: RSA' \ + 'Subkey-Length: 2048' \ + 'Name-Real: Hubble CI Release Gate' \ + 'Name-Email: hubble-ci-release-gate@example.invalid' \ + 'Expire-Date: 0' \ + '%commit' \ + > "${RUNNER_TEMP}/hubble-release-gate-key.conf" + gpg --batch --generate-key "${RUNNER_TEMP}/hubble-release-gate-key.conf" + gpg --batch --yes --armor --detach-sign "${HUBBLE_TARBALL}" + bash "${TRAVIS_DIR}/check-hubble-dist.sh" "${HUBBLE_TARBALL}" \ + --require-sidecars \ + --json-output hugegraph-hubble/target/hubble-dist-check.json - name: Unit test run: mvn test -P unit-test -pl hugegraph-hubble/hubble-be -ntp - name: API test - env: - CI: false run: | - cd hugegraph-hubble && ls - hubble-dist/assembly/travis/run-api-test.sh + cd hugegraph-hubble + HUBBLE_TARBALL="$(ls target/apache-hugegraph-hubble-*.tar.gz | head -n 1)" + HUBBLE_694_EVIDENCE_DIR=target/hubble-live-acceptance \ + hubble-dist/assembly/travis/verify-hubble-issue-694.sh \ + "${HUBBLE_TARBALL}" http://127.0.0.1:8080 + + - name: Upload Hubble smoke evidence + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: hubble-smoke-java-${{ matrix.JAVA_VERSION }}-${{ github.run_attempt }} + path: | + hugegraph-hubble/target/hubble-dist-check.json + hugegraph-hubble/target/hubble-live-acceptance/**/*.json + hugegraph-hubble/target/hubble-live-acceptance/**/*.png + hugegraph-hubble/target/hubble-live-acceptance/**/*.log + if-no-files-found: warn + retention-days: 14 - name: Upload coverage to Codecov uses: ./.github/actions/upload-coverage diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 18cb05b4a..26285abb5 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -28,7 +28,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@v6 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index ef51a265e..3b2d3d701 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -23,7 +23,7 @@ on: push: branches: - master - - /^release-.*$/ + - 'release-*' pull_request: jobs: @@ -31,10 +31,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Fetch Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 # More info could refer to: https://github.com/apache/skywalking-eyes - name: Check License Header - uses: apache/skywalking-eyes@main + uses: apache/skywalking-eyes@29bd646002b63bb4c0ed1648d1654c35fb8ff027 # main 2026-07-12 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -54,14 +54,14 @@ jobs: USE_STAGE: 'true' # Whether to include the stage repository. steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: '11' - distribution: 'adopt' + distribution: 'temurin' - name: Use Node.js 16 - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: node-version: '16' diff --git a/.github/workflows/loader-ci.yml b/.github/workflows/loader-ci.yml index a7b085ab3..f8eee02d3 100644 --- a/.github/workflows/loader-ci.yml +++ b/.github/workflows/loader-ci.yml @@ -5,7 +5,7 @@ on: push: branches: - master - - /^release-.*$/ + - 'release-*' paths: - hugegraph-loader/** - hugegraph-dist/** @@ -43,7 +43,7 @@ jobs: steps: - name: Fetch Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 2 @@ -55,17 +55,17 @@ jobs: uses: ./.github/actions/setup-java-env with: java-version: ${{ matrix.JAVA_VERSION }} - distribution: 'adopt' + distribution: 'temurin' use-stage: ${{ env.USE_STAGE }} - name: Cache Hadoop - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/hadoop-3.3.6.tar.gz key: ${{ runner.os }}-hadoop-3.3.6 - name: Cache HugeGraph Server - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/hugegraph-cache-${{ steps.get-commit.outputs.commit_id }} key: ${{ runner.os }}-hugegraph-server-${{ steps.get-commit.outputs.commit_id }} diff --git a/.github/workflows/spark-connector-ci.yml b/.github/workflows/spark-connector-ci.yml index b61b56e2f..50693024a 100644 --- a/.github/workflows/spark-connector-ci.yml +++ b/.github/workflows/spark-connector-ci.yml @@ -5,7 +5,7 @@ on: push: branches: - master - - /^release-.*$/ + - 'release-*' paths: - hugegraph-spark-connector/** - hugegraph-dist/** @@ -31,7 +31,7 @@ jobs: steps: - name: Fetch Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 2 @@ -43,7 +43,7 @@ jobs: uses: ./.github/actions/setup-java-env with: java-version: ${{ matrix.JAVA_VERSION }} - distribution: 'adopt' + distribution: 'temurin' use-stage: ${{ env.USE_STAGE }} - name: Compile diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 64b62dc5a..c46000e85 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v8 + - uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Due to the lack of activity, the current issue is marked as stale and will be closed after 20 days, any update will remove the stale label' diff --git a/.github/workflows/tools-ci.yml b/.github/workflows/tools-ci.yml index dda37f3e3..340f71aa1 100644 --- a/.github/workflows/tools-ci.yml +++ b/.github/workflows/tools-ci.yml @@ -5,7 +5,7 @@ on: push: branches: - master - - /^release-.*$/ + - 'release-*' paths: - hugegraph-tools/** - hugegraph-dist/** @@ -31,7 +31,7 @@ jobs: steps: - name: Fetch Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 2 @@ -43,7 +43,7 @@ jobs: uses: ./.github/actions/setup-java-env with: java-version: ${{ matrix.JAVA_VERSION }} - distribution: 'adopt' + distribution: 'temurin' use-stage: ${{ env.USE_STAGE }} - name: Compile diff --git a/.gitignore b/.gitignore index 528ddad73..1467dcafc 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,25 @@ Thumbs.db go.env # AI-IDE prompt files (We only keep AGENTS.md, other files could soft-linked it when needed) +# Local AI-agent workspaces and run evidence +.codex-task/* +!.codex-task/GLOBAL-TODO.md +.autonomous/ +.claude/ +superpowers/ + +# Local Hubble runtime / extracted distributions +/db*.db +/db*.trace.db +/logs/ +/hugegraph-hubble/apache-hugegraph-hubble-*/ +/hugegraph-hubble/hubble-dist/apache-hugegraph-hubble-*/ +/hugegraph-loader/apache-hugegraph-loader-*/ + +# Local dependency analysis reports +/hugegraph-dist/scripts/dependency/current-dependencies.txt +/hugegraph-dist/scripts/dependency/result.txt + # Claude Projects CLAUDE.md CLAUDE_*.md @@ -124,5 +143,4 @@ codeium-instructions.md .ai-instructions.md *.ai-prompt.md WARP.md - - +/.agents diff --git a/.serena/project.yml b/.serena/project.yml index edbab5745..4fec60fbc 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,22 +1,29 @@ - -# list of languages for which language servers are started; choose from: -# al bash clojure cpp csharp -# csharp_omnisharp dart elixir elm erlang -# fortran fsharp go groovy haskell -# haxe java julia kotlin lua -# markdown -# matlab nix pascal perl php -# php_phpactor powershell python python_jedi r +# list of languages for which language servers are started (LSP backend only); choose from: +# ada al angular ansible bash +# bsl clojure cpp cpp_ccls crystal +# csharp csharp_omnisharp cue dart elixir +# elm erlang fortran fsharp gdscript +# go groovy haskell haxe hlsl +# html java json julia kotlin +# latex lean4 lua luau markdown +# matlab msl nix ocaml pascal +# perl php php_phpactor php_phpantom powershell +# python python_jedi python_pyrefly python_ty r # rego ruby ruby_solargraph rust scala -# swift terraform toml typescript typescript_vts -# vue yaml zig -# (This list may be outdated. For the current list, see values of Language enum here: -# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py -# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# scss solidity svelte swift systemverilog +# terraform toml typescript typescript_vts vue +# yaml zig +# (This list may be outdated; generated with scripts/print_language_list.py; +# For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py) +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) # Note: # - For C, use cpp # - For JavaScript, use typescript +# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) +# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) +# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) # - For Free Pascal/Lazarus, use pascal # Special requirements: # Some languages require additional setup/installations. @@ -46,56 +53,18 @@ read_only: false # list of tool names to exclude. # This extends the existing exclusions (e.g. from the global configuration) -# -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project based on the project name or path. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly, -# for example by saying that the information retrieved from a memory file is no longer correct -# or no longer relevant for the project. -# * `edit_memory`: Replaces content matching a regular expression in a memory. -# * `execute_shell_command`: Executes a shell command. -# * `find_file`: Finds files in the given relative paths -# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend -# * `find_symbol`: Performs a global (or local) search using the language server backend. -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') -# for clients that do not read the initial instructions when the MCP server is connected. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Read the content of a memory file. This tool should only be used if the information -# is relevant to the current task. You can infer whether the information -# is relevant from the memory file name. -# You should not read the same memory file multiple times in the same conversation. -# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported -# (e.g., renaming "global/foo" to "bar" moves it from global to project scope). -# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities. -# For JB, we use a separate tool. -# * `replace_content`: Replaces content in a file (optionally using regular expressions). -# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend. -# * `safe_delete_symbol`: -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format. -# The memory name should be meaningful. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html excluded_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: "" -# the name by which the project can be referenced within Serena +# the name by which the project can be referenced within Serena/when chatting with the LLM. project_name: "toolchain" # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). # This extends the existing inclusions (e.g. from the global configuration). +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html included_optional_tools: [] # list of mode names to that are always to be included in the set of active modes @@ -106,15 +75,19 @@ included_optional_tools: [] # Set this to a list of mode names to always include the respective modes for this project. base_modes: [] -# list of mode names that are to be activated by default. -# The full set of modes to be activated is base_modes + default_modes. -# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# list of mode names that are to be activated by default, overriding the setting in the global configuration. +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply. # Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply +# for this project. # This setting can, in turn, be overridden by CLI parameters (--mode). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes default_modes: [] # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. # This cannot be combined with non-empty excluded_tools or included_optional_tools. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html fixed_tools: [] # time budget (seconds) per tool call for the retrieval of additional symbol information @@ -149,6 +122,47 @@ ignored_memory_patterns: [] # advanced configuration option allowing to configure language server-specific options. # Maps the language key to the options. -# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. -# No documentation on options means no options are available. +# The settings are considered only if the project is trusted (see global configuration to define trusted projects). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings ls_specific_settings: {} + +# list of mode names to be activated additionally for this project, e.g. ["query-projects"] +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes +added_modes: + +# list of workspace folder paths (LSP backend only). +# These folders will be used to build up Serena's symbol index. +# Paths must be within the project root and should thus be relative to the project root. +# Furthermore, the paths should not be filtered by ignore settings. +# Default setting: The entire project root folder (".") is considered. +# In (large) monorepos, this can be used to index only subfolders of the project root, e.g. +# ls_workspace_folders: +# - "./subproject1" +# - "./subproject2" +ls_workspace_folders: +- . + +# list of additional workspace folder paths for cross-package reference support. +# Paths can be absolute or relative to the project root. +# Each folder is registered as an LSP workspace folder, enabling language servers to discover +# symbols and references across package boundaries, but these folders are not indexed by Serena, +# i.e. the respective symbols will not be found using Serena's symbol search tools. +# Example: +# additional_workspace_folders: +# - ../sibling-package +# - ../shared-lib +ls_additional_workspace_folders: [] + +# optional shell command to run before the language backend (LSP or JetBrains) is initialised. +# the command runs in the project root directory and is only executed if the project is trusted +# (see trusted_project_path_patterns in the global configuration). +# serena waits for the command to exit: a non-zero exit code is logged as an error but does not +# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety +# backstop for non-terminating commands; on expiry the process is killed and activation continues. +# example: activation_command: "npx nx run-many -t build" +activation_command: + +# maximum time in seconds to wait for activation_command to complete before killing it (default 180s). +# must be a positive number. +activation_command_timeout: 180.0 diff --git a/AGENTS.md b/AGENTS.md index f398d6ad6..4aa6cceaa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file provides guidance to AI coding assistants (Claude Code, Cursor, GitHub ## Project Overview -Apache HugeGraph Toolchain - a multi-module Maven project providing utilities for the HugeGraph graph database. Current version: 1.7.0. +Apache HugeGraph Toolchain - a multi-module Maven project providing utilities for the HugeGraph graph database. It relies on the Java Client to power loaders, CLI tools, and the modernized Hubble platform. ## Build Commands @@ -130,22 +130,6 @@ hugegraph-loader, hugegraph-tools, hugegraph-hubble, hugegraph-spark-connector | tools | `hugegraph-tools/src/main/java` | `org.apache.hugegraph` | | spark | `hugegraph-spark-connector/src/main/scala` | `org.apache.hugegraph.spark` | -## Running Applications - -### Hubble (Web UI on port 8088) -```bash -cd hugegraph-hubble/apache-hugegraph-hubble-*/bin -./start-hubble.sh # Background -./start-hubble.sh -f # Foreground -./stop-hubble.sh # Stop -``` - -### Loader -```bash -cd hugegraph-loader/apache-hugegraph-loader-* -./bin/hugegraph-loader.sh [options] -``` - ## Docker ```bash diff --git a/README.md b/README.md index 3d848a2ec..322e6bda5 100644 --- a/README.md +++ b/README.md @@ -234,12 +234,12 @@ client := hugegraph.NewClient("http://localhost:8080", "hugegraph") **Purpose**: Web-based graph management and visualization platform **Key Features**: -- Schema management with visual editor -- Data loading interface -- Graph visualization and exploration -- Gremlin query console -- Multi-graph workspace management -- User authentication and authorization +- Multi-graph workspace & connection management +- Interactive schema management with graphical editor +- Comprehensive data loading dashboard +- Dynamic graph visualization with path and topology canvas +- Built-in Gremlin query console & algorithm explorer +- Fine-grained user authentication & multi-language localization (i18n) **Technology Stack**: Spring Boot + React + TypeScript + MobX + Ant Design diff --git a/hugegraph-client/pom.xml b/hugegraph-client/pom.xml index 745a37370..09895cc0d 100644 --- a/hugegraph-client/pom.xml +++ b/hugegraph-client/pom.xml @@ -61,12 +61,10 @@ org.apache.hugegraph hg-pd-client - 1.5.0 org.apache.hugegraph hg-pd-grpc - 1.5.0 @@ -78,10 +76,9 @@ ${compiler.source} ${compiler.target} - - 500 - + -Xmaxerrs + 500 -Xlint:unchecked diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java b/hugegraph-client/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java index 018c8dbe6..ff43a01c9 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java @@ -69,6 +69,12 @@ public List list(int limit) { return result.readList(this.type(), User.class); } + public User getByName(String name) { + Map params = ImmutableMap.of("name", name); + RestResult result = this.client.get(this.path(), params); + return result.readObject(User.class); + } + public User update(User user) { String id = formatEntityId(user.id()); RestResult result = this.client.put(this.path(), id, user); diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/api/graphs/GraphsAPI.java b/hugegraph-client/src/main/java/org/apache/hugegraph/api/graphs/GraphsAPI.java index 030cf1260..073adaf23 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/api/graphs/GraphsAPI.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/api/graphs/GraphsAPI.java @@ -18,6 +18,7 @@ package org.apache.hugegraph.api.graphs; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -109,19 +110,20 @@ public List> listProfile(String prefix) { return profiles; } - public Map setDefault(String name) { + public Map setDefault(String name) { String defaultPath = joinPath(this.path(), name, "default"); - RestResult result = this.client.get(defaultPath); + RestResult result = this.client.post(defaultPath, Collections.emptyMap()); return result.readObject(Map.class); } - public Map unSetDefault(String name) { - String unDefaultPath = joinPath(this.path(), name, "undefault"); - RestResult result = this.client.get(unDefaultPath); + public Map unSetDefault(String name) { + String unDefaultPath = joinPath(this.path(), name, "default"); + RestResult result = this.client.delete(unDefaultPath, + Collections.emptyMap()); return result.readObject(Map.class); } - public Map getDefault() { + public Map getDefault() { String defaultPath = joinPath(this.path(), "default"); RestResult result = this.client.get(defaultPath); return result.readObject(Map.class); @@ -170,6 +172,11 @@ public void drop(String graph, String message) { ImmutableMap.of(CONFIRM_MESSAGE, message)); } + /** + * @deprecated Current Server per-graph management accepts only update. + * Use {@link #reload()} for a whole-server graph reload. + */ + @Deprecated public Map reload(String name) { RestResult result = this.client.put(this.path(), name, ImmutableMap.of("action", "reload")); diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java b/hugegraph-client/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java index b04408fda..ab611a3f2 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java @@ -131,8 +131,16 @@ public Map deleteDefaultRole(String name, String user, if (StringUtils.isNotEmpty(graph)) { params.put("graph", graph); } - RestResult result = this.client.delete(path, params); - return result.readObject(Map.class); + this.client.delete(path, params); + + Map result = new HashMap<>(); + result.put("user", user); + result.put("role", role); + result.put("graphSpace", name); + if (StringUtils.isNotEmpty(graph)) { + result.put("graph", graph); + } + return result; } public void delete(String name) { diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/client/RestClient.java b/hugegraph-client/src/main/java/org/apache/hugegraph/client/RestClient.java index f8c57ec18..ca1dcfad3 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/client/RestClient.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/client/RestClient.java @@ -183,7 +183,12 @@ public RestResult get(String path, Map params) { @Override protected void checkStatus(okhttp3.Response response, int... statuses) { - boolean match = false; + // HugeGraph Server returns 200 with the updated resource for some + // successful DELETE operations, while hugegraph-common only passes + // 202/204 as the expected DELETE statuses. + boolean deleteOk = response.code() == 200 && + "DELETE".equals(response.request().method()); + boolean match = deleteOk; for (int status : statuses) { if (status == response.code()) { match = true; diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java index 9f85ec4a4..6d84fdb44 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java @@ -132,6 +132,10 @@ public User getUser(Object id) { return this.userAPI.get(id); } + public User getUserByName(String name) { + return this.userAPI.getByName(name); + } + public User.UserRole getUserRole(Object id) { return this.userAPI.getUserRole(id); } diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/GraphsManager.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/GraphsManager.java index aacf261f8..920dfb684 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/GraphsManager.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/GraphsManager.java @@ -77,15 +77,15 @@ public List> listProfile(String prefix) { return this.graphsAPI.listProfile(prefix); } - public Map setDefault(String name) { + public Map setDefault(String name) { return this.graphsAPI.setDefault(name); } - public Map unSetDefault(String name) { + public Map unSetDefault(String name) { return this.graphsAPI.unSetDefault(name); } - public Map getDefault() { + public Map getDefault() { return this.graphsAPI.getDefault(); } @@ -103,6 +103,11 @@ public void dropGraph(String graph, String message) { this.graphsAPI.drop(graph, message); } + /** + * @deprecated Current Server per-graph management accepts only update. + * Use {@link #reload()} for a whole-server graph reload. + */ + @Deprecated public void reload(String graph) { this.graphsAPI.reload(graph); } diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java index c681a76a9..6327b0023 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java @@ -108,7 +108,10 @@ public HugeClient(HugeClientBuilder builder) { public HugeClient(HugeClient client, String graphSpace, String graph) { this.borrowedClient = true; this.client = client.client; - this.initManagers(this.client, graphSpace, graph); + this.graphSpaceName = (graphSpace == null || graphSpace.isEmpty()) ? + HugeClientBuilder.DEFAULT_GRAPHSPACE : graphSpace; + this.graphName = graph; + this.initManagers(this.client, this.graphSpaceName, this.graphName); } public static HugeClientBuilder builder(String url, String graphSpace, String graph) { @@ -119,8 +122,14 @@ public static HugeClientBuilder builder(String url, String graph) { return new HugeClientBuilder(url, HugeClientBuilder.DEFAULT_GRAPHSPACE, graph); } + public static HugeClientBuilder builder(String url, String graphSpace, String graph, + boolean skipRequiredChecks) { + return new HugeClientBuilder(url, graphSpace, graph).graphRequired(!skipRequiredChecks); + } + public HugeClient assignGraph(String graphSpace, String graph) { - this.graphSpaceName = graphSpace; + this.graphSpaceName = (graphSpace == null || graphSpace.isEmpty()) ? + HugeClientBuilder.DEFAULT_GRAPHSPACE : graphSpace; this.graphName = graph; this.initManagers(this.client, this.graphSpaceName, this.graphName); return this; diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClientBuilder.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClientBuilder.java index 2b5d547b3..545566243 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClientBuilder.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClientBuilder.java @@ -54,7 +54,8 @@ public class HugeClientBuilder { public HugeClientBuilder(String url, String graphSpace, String graph) { this.url = url; - this.graphSpace = graphSpace; + this.graphSpace = (graphSpace == null || graphSpace.isEmpty()) ? + DEFAULT_GRAPHSPACE : graphSpace; this.graph = graph; this.username = ""; this.password = ""; @@ -77,8 +78,8 @@ public HugeClient build() { "Expect a string value as the url parameter argument, but got: %s", this.url); E.checkArgument(this.graph != null && !this.graph.isEmpty(), - "Expect a string value as the graph name parameter argument, but got: %s", - this.graph); + "Expect a string value as the graph name " + + "parameter argument, but got: %s", this.graph); } return new HugeClient(this); } @@ -89,7 +90,8 @@ public HugeClientBuilder graphRequired(boolean graphRequired) { } public HugeClientBuilder configGraphSpace(String graphSpace) { - this.graphSpace = graphSpace; + this.graphSpace = (graphSpace == null || graphSpace.isEmpty()) ? + DEFAULT_GRAPHSPACE : graphSpace; return this; } diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/BaseClientTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/BaseClientTest.java index 2fda81d66..50cedf646 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/BaseClientTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/BaseClientTest.java @@ -55,6 +55,7 @@ public class BaseClientTest { protected static final String GRAPH = "hugegraph"; protected static final String USERNAME = "admin"; protected static final String PASSWORD = "pa"; + //protected static final String PASSWORD = "admin"; protected static final int TIMEOUT = 10; private static HugeClient client; diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/api/TaskApiTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/api/TaskApiTest.java index 27da38211..9e7b07201 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/api/TaskApiTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/api/TaskApiTest.java @@ -263,20 +263,14 @@ public void testCancel() { long taskId = gremlin().executeAsTask(request); // Wait for task to start - try { - Thread.sleep(300); - } catch (InterruptedException ignored) { - } - + sleep(300L); + // Cancel async task Task task = taskAPI.cancel(taskId); Assert.assertTrue(task.cancelling()); // Wait for cancellation to complete - try { - Thread.sleep(500); - } catch (InterruptedException ignored) { - } + sleep(500L); task = taskAPI.get(taskId); Assert.assertTrue(task.cancelled()); @@ -304,4 +298,13 @@ public void testTaskAsMap() { Assert.assertEquals("rebuild_index", taskMap.get(Task.P.TYPE)); Assert.assertEquals("success", taskMap.get(Task.P.STATUS)); } + + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Assert.fail(e.getMessage()); + } + } } diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LoginApiTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LoginApiTest.java index e1dc90d7d..ae1f17e03 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LoginApiTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LoginApiTest.java @@ -26,6 +26,7 @@ import org.apache.hugegraph.testutil.Assert; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; public class LoginApiTest extends AuthApiTest { diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LogoutApiTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LogoutApiTest.java index 24c777807..de4549d04 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LogoutApiTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/LogoutApiTest.java @@ -28,6 +28,7 @@ import org.apache.hugegraph.testutil.Whitebox; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; public class LogoutApiTest extends AuthApiTest { diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/TokenApiTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/TokenApiTest.java index 9dcec5a30..b0d8407cb 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/TokenApiTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/api/auth/TokenApiTest.java @@ -29,6 +29,7 @@ import org.apache.hugegraph.testutil.Whitebox; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; public class TokenApiTest extends AuthApiTest { diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/functional/AuthManagerTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/functional/AuthManagerTest.java index 243cb1f17..6b4f82860 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/functional/AuthManagerTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/functional/AuthManagerTest.java @@ -41,6 +41,7 @@ import org.apache.hugegraph.testutil.Assert; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import com.google.common.collect.ImmutableSet; diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/GraphSpaceAPITest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/GraphSpaceAPITest.java new file mode 100644 index 000000000..1ffc87a0e --- /dev/null +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/GraphSpaceAPITest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.Map; + +import org.apache.hugegraph.api.space.GraphSpaceAPI; +import org.apache.hugegraph.client.RestClient; +import org.apache.hugegraph.rest.RestResult; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +public class GraphSpaceAPITest extends BaseUnitTest { + + private RestClient mockClient; + private GraphSpaceAPI graphSpaceAPI; + + @Before + public void setup() { + this.mockClient = Mockito.mock(RestClient.class); + this.graphSpaceAPI = new GraphSpaceAPI(this.mockClient); + } + + @Test + public void testDeleteDefaultRoleDoesNotReadEmptyResponseBody() { + RestResult mockResult = Mockito.mock(RestResult.class); + Mockito.when(mockResult.readObject(Map.class)) + .thenThrow(new AssertionError("204 response has no body")); + + ArgumentCaptor pathCaptor = + ArgumentCaptor.forClass(String.class); + @SuppressWarnings("unchecked") + ArgumentCaptor> paramsCaptor = + ArgumentCaptor.forClass(Map.class); + + Mockito.when(this.mockClient.delete(pathCaptor.capture(), + paramsCaptor.capture())) + .thenReturn(mockResult); + + Map response = this.graphSpaceAPI.deleteDefaultRole( + "DEFAULT", "admin", "SPACE", ""); + + Assert.assertEquals("graphspaces/DEFAULT/role", pathCaptor.getValue()); + Assert.assertEquals("admin", paramsCaptor.getValue().get("user")); + Assert.assertEquals("SPACE", paramsCaptor.getValue().get("role")); + Assert.assertEquals("admin", response.get("user")); + Assert.assertEquals("SPACE", response.get("role")); + Assert.assertEquals("DEFAULT", response.get("graphSpace")); + } +} diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/GraphsAPITest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/GraphsAPITest.java index d10f84f45..ac99cd9fb 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/GraphsAPITest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/GraphsAPITest.java @@ -21,6 +21,7 @@ import org.apache.hugegraph.api.graphs.GraphsAPI; import org.apache.hugegraph.client.RestClient; +import org.apache.hugegraph.driver.GraphsManager; import org.apache.hugegraph.rest.RestHeaders; import org.apache.hugegraph.rest.RestResult; import org.apache.hugegraph.testutil.Assert; @@ -107,4 +108,63 @@ public void testCloneGraphUsesJsonContentTypeAndParams() { Assert.assertEquals("source-graph", capturedParams.get("clone_graph_name")); } + + @Test + public void testSetDefaultGraphUsesCanonicalPost() { + RestResult mockResult = Mockito.mock(RestResult.class); + Mockito.when(mockResult.readObject(Map.class)) + .thenReturn(null); + + ArgumentCaptor pathCaptor = + ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(Object.class); + + Mockito.when(this.mockClient.post(pathCaptor.capture(), + bodyCaptor.capture())) + .thenReturn(mockResult); + + this.graphsAPI.setDefault("test-graph"); + + Assert.assertEquals("graphspaces/DEFAULT/graphs/test-graph/default", + pathCaptor.getValue()); + Assert.assertTrue(((Map) bodyCaptor.getValue()).isEmpty()); + Mockito.verify(mockResult).readObject(Map.class); + } + + @Test + public void testUnsetDefaultGraphUsesCanonicalDelete() { + RestResult mockResult = Mockito.mock(RestResult.class); + Mockito.when(mockResult.readObject(Map.class)) + .thenReturn(null); + + ArgumentCaptor pathCaptor = + ArgumentCaptor.forClass(String.class); + @SuppressWarnings("unchecked") + ArgumentCaptor> paramsCaptor = + ArgumentCaptor.forClass(Map.class); + + Mockito.when(this.mockClient.delete(pathCaptor.capture(), + paramsCaptor.capture())) + .thenReturn(mockResult); + + this.graphsAPI.unSetDefault("test-graph"); + + Assert.assertEquals("graphspaces/DEFAULT/graphs/test-graph/default", + pathCaptor.getValue()); + Assert.assertTrue(paramsCaptor.getValue().isEmpty()); + Mockito.verify(mockResult).readObject(Map.class); + } + + @Test + public void testPerGraphReloadIsDeprecated() throws NoSuchMethodException { + Assert.assertNotNull(GraphsAPI.class.getMethod("reload", String.class) + .getAnnotation(Deprecated.class)); + Assert.assertNotNull(GraphsManager.class.getMethod("reload", String.class) + .getAnnotation(Deprecated.class)); + Assert.assertNull(GraphsAPI.class.getMethod("reload") + .getAnnotation(Deprecated.class)); + Assert.assertNull(GraphsManager.class.getMethod("reload") + .getAnnotation(Deprecated.class)); + } } diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/RestClientStatusTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/RestClientStatusTest.java new file mode 100644 index 000000000..a3f8fa807 --- /dev/null +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/RestClientStatusTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.hugegraph.unit; + +import org.apache.hugegraph.client.RestClient; +import org.apache.hugegraph.exception.ServerException; +import org.junit.Test; +import org.mockito.Mockito; + +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.ResponseBody; + +public class RestClientStatusTest extends BaseUnitTest { + + @Test + public void testOkIsAcceptedForDeleteSuccessStatuses() { + okhttp3.Response response = Mockito.mock(okhttp3.Response.class); + Request request = new Request.Builder().url("http://127.0.0.1:1") + .delete() + .build(); + Mockito.when(response.code()).thenReturn(200); + Mockito.when(response.request()).thenReturn(request); + + new TestRestClient().check(response, 202, 204); + } + + @Test(expected = ServerException.class) + public void testOkIsRejectedForNonDeleteExpectedStatus() { + Request request = new Request.Builder().url("http://127.0.0.1:1") + .post(okhttp3.RequestBody.create(null, + new byte[0])) + .build(); + okhttp3.Response response = new okhttp3.Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create( + MediaType.parse("application/json"), "{}")) + .build(); + + new TestRestClient().check(response, 201, 202); + } + + private static class TestRestClient extends RestClient { + + TestRestClient() { + super("http://127.0.0.1:1", "", "", 1); + } + + void check(okhttp3.Response response, int... statuses) { + this.checkStatus(response, statuses); + } + } +} diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java index 5ddb23a50..45670a92f 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -25,9 +25,11 @@ VertexSerializerTest.class, PathSerializerTest.class, RestResultTest.class, + RestClientStatusTest.class, BatchElementRequestTest.class, PropertyKeyTest.class, IndexLabelTest.class, + GraphSpaceAPITest.class, GraphsAPITest.class, CommonUtilTest.class, IdUtilTest.class, diff --git a/hugegraph-dist/scripts/dependency/known-dependencies.txt b/hugegraph-dist/scripts/dependency/known-dependencies.txt index 0b38c41e2..ba38b94c2 100644 --- a/hugegraph-dist/scripts/dependency/known-dependencies.txt +++ b/hugegraph-dist/scripts/dependency/known-dependencies.txt @@ -1,6 +1,5 @@ -HdrHistogram-2.1.9.jar HikariCP-3.2.0.jar -LatencyUtils-2.0.3.jar +HikariCP-java7-2.4.12.jar ST4-4.0.4.jar accessors-smart-1.2.jar accessors-smart-2.4.2.jar @@ -9,10 +8,14 @@ aircompressor-0.21.jar animal-sniffer-annotations-1.19.jar annotations-13.0.jar annotations-17.0.0.jar +annotations-24.0.1.jar annotations-4.1.1.4.jar +ansj_seg-5.1.6.jar ant-1.9.1.jar ant-launcher-1.9.1.jar antlr-runtime-3.5.2.jar +aopalliance-1.0.jar +aopalliance-repackaged-2.5.0-b42.jar apache-curator-2.12.0.pom arrow-format-0.8.0.jar arrow-format-2.0.0.jar @@ -20,19 +23,30 @@ arrow-memory-0.8.0.jar arrow-memory-core-2.0.0.jar arrow-vector-0.8.0.jar arrow-vector-2.0.0.jar -asm-5.0.4.jar +asm-6.0.jar asm-8.0.1.jar audience-annotations-0.5.0.jar +auto-service-annotations-1.0.jar +automaton-1.11-8.jar +avatica-1.11.0.jar avro-1.10.2.jar avro-1.7.7.jar +bolt-1.6.4.jar caffeine-2.6.2.jar +caffeine-2.8.0.jar +calcite-core-1.16.0.jar +calcite-druid-1.16.0.jar +calcite-linq4j-1.16.0.jar +checker-qual-2.10.0.jar checker-qual-3.5.0.jar classmate-1.4.0.jar commons-beanutils-1.9.4.jar commons-cli-1.3.1.jar commons-codec-1.15.jar commons-collections-3.2.2.jar +commons-collections4-4.2.jar commons-collections4-4.4.jar +commons-compiler-2.7.6.jar commons-compress-1.21.jar commons-configuration-1.10.jar commons-configuration2-2.1.1.jar @@ -40,6 +54,8 @@ commons-configuration2-2.8.0.jar commons-crypto-1.0.0.jar commons-crypto-1.1.0.jar commons-daemon-1.0.13.jar +commons-dbcp-1.4.jar +commons-fileupload-1.4.jar commons-fileupload-1.5.jar commons-io-2.8.0.jar commons-lang-2.6.jar @@ -47,6 +63,7 @@ commons-lang3-3.9.jar commons-logging-1.1.3.jar commons-math3-3.4.1.jar commons-net-3.9.0.jar +commons-pool-1.6.jar commons-text-1.10.0.jar curator-client-4.2.0.jar curator-framework-2.12.0.jar @@ -55,23 +72,56 @@ curator-recipes-2.13.0.jar curator-recipes-4.2.0.jar datanucleus-core-4.1.17.jar disruptor-3.3.6.jar +disruptor-3.3.7.jar dnsjava-2.1.7.jar dropwizard-metrics-hadoop-metrics2-reporter-0.1.2.jar +eclipse-collections-11.1.0.jar +eclipse-collections-api-11.1.0.jar +ehcache-3.6.3.jar +elasticsearch-java-7.16.1.jar +elasticsearch-rest-client-6.4.3.jar +error_prone_annotations-2.3.3.jar error_prone_annotations-2.3.4.jar +esri-geometry-api-2.0.0.jar +exp4j-0.4.8.jar +failsafe-2.4.1.jar failureaccess-1.0.1.jar +fastutil-8.5.9.jar findbugs-annotations-1.3.9-1.jar flatbuffers-1.2.0-3f79e055.jar flatbuffers-java-1.9.0.jar +fst-2.50.jar +fury-core-0.9.0.jar +generex-1.0.2.jar +geronimo-jcache_1.0_spec-1.0-alpha-1.jar +gremlin-core-3.5.1.jar +gremlin-driver-3.5.1.jar +gremlin-groovy-3.5.1.jar +gremlin-shaded-3.5.1.jar +gremlin-test-3.5.1.jar +groovy-2.5.14-indy.jar groovy-all-2.4.21.jar +groovy-cli-picocli-2.5.8.jar +groovy-console-2.5.8.jar +groovy-groovysh-2.5.14-indy.jar +groovy-json-2.5.14-indy.jar +groovy-jsr223-2.5.14-indy.jar +groovy-swing-2.5.8.jar +groovy-templates-2.5.8.jar +groovy-xml-2.5.8.jar grpc-api-1.39.0.jar grpc-context-1.39.0.jar grpc-core-1.39.0.jar +grpc-grpclb-1.39.0.jar +grpc-netty-1.39.0.jar grpc-netty-shaded-1.39.0.jar grpc-protobuf-1.39.0.jar grpc-protobuf-lite-1.39.0.jar grpc-stub-1.39.0.jar gson-2.8.9.jar guava-30.0-jre.jar +guice-4.0.jar +guice-servlet-4.0.jar h2-1.4.199.jar hadoop-annotations-3.3.1.jar hadoop-auth-3.3.1.jar @@ -84,9 +134,18 @@ hadoop-mapreduce-client-core-3.3.1.jar hadoop-mapreduce-client-jobclient-3.3.1.jar hadoop-shaded-guava-1.1.1.jar hadoop-shaded-protobuf_3_7-1.1.1.jar +hadoop-yarn-api-3.1.0.jar hadoop-yarn-api-3.3.1.jar hadoop-yarn-client-3.3.1.jar +hadoop-yarn-common-3.1.0.jar hadoop-yarn-common-3.3.1.jar +hadoop-yarn-registry-3.1.0.jar +hadoop-yarn-server-applicationhistoryservice-3.1.0.jar +hadoop-yarn-server-common-3.1.0.jar +hadoop-yarn-server-resourcemanager-3.1.0.jar +hadoop-yarn-server-web-proxy-3.1.0.jar +hamcrest-2.2.jar +hanlp-portable-1.8.3.jar hbase-client-2.2.3.jar hbase-common-2.2.3.jar hbase-hadoop-compat-2.2.3.jar @@ -105,12 +164,14 @@ hbase-shaded-miscellaneous-2.2.1.jar hbase-shaded-netty-2.2.1.jar hbase-shaded-protobuf-2.2.1.jar hbase-zookeeper-2.2.3.jar -hg-pd-client-1.5.0.jar -hg-pd-common-1.5.0.jar -hg-pd-grpc-1.5.0.jar +hessian-3.3.6.jar +hg-pd-client-1.7.0.jar +hg-pd-common-1.7.0.jar +hg-pd-grpc-1.7.0.jar hibernate-validator-6.0.17.Final.jar hive-classification-3.1.3.jar hive-common-3.1.3.jar +hive-exec-3.1.3-core.jar hive-exec-3.1.3.jar hive-llap-client-3.1.3.jar hive-llap-common-3.1.3.jar @@ -125,13 +186,20 @@ hive-storage-api-2.7.0.jar hive-storage-api-2.7.2.jar hive-upgrade-acid-3.1.3.jar hive-vector-code-gen-3.1.3.jar +hk2-api-2.5.0-b42.jar +hk2-locator-2.5.0-b42.jar +hk2-utils-2.5.0-b42.jar +hppc-0.7.1.jar hppc-0.7.2.jar htrace-core4-4.1.0-incubating.jar htrace-core4-4.2.0-incubating.jar +httpasyncclient-4.1.4.jar httpclient-4.5.13.jar httpclient-4.5.9.jar httpcore-4.4.12.jar httpcore-4.4.13.jar +httpcore-nio-4.4.12.jar +ikanalyzer-2012_u6.jar ivy-2.4.0.jar ivy-2.5.0.jar j2objc-annotations-1.3.jar @@ -139,24 +207,35 @@ jackson-annotations-2.12.3.jar jackson-core-2.12.3.jar jackson-core-asl-1.9.13.jar jackson-databind-2.12.3.jar +jackson-dataformat-yaml-2.12.3.jar jackson-datatype-jdk8-2.12.3.jar jackson-datatype-jsr310-2.12.3.jar +jackson-jaxrs-1.9.2.jar jackson-jaxrs-base-2.12.3.jar jackson-jaxrs-json-provider-2.12.3.jar jackson-mapper-asl-1.9.13.jar jackson-module-jaxb-annotations-2.12.3.jar jackson-module-parameter-names-2.12.3.jar +jackson-xc-1.9.2.jar jakarta.activation-2.0.1.jar jakarta.activation-api-1.2.1.jar +jakarta.json-2.0.1.jar jakarta.xml.bind-api-2.3.2.jar jamon-runtime-2.4.1.jar +janino-3.0.15.jar +java-util-1.9.0.jar +javacsv-2.0.jar +javapoet-1.8.0.jar +javassist-3.22.0-CR2.jar javassist-3.24.0-GA.jar javassist-3.25.0-GA.jar javassist-3.28.0-GA.jar +javatuples-1.2.jar javax.activation-api-1.2.0.jar javax.annotation-api-1.3.2.jar javax.el-3.0.0.jar javax.el-3.0.1-b12.jar +javax.inject-1.jar javax.inject-2.5.0-b32.jar javax.inject-2.5.0-b42.jar javax.json-1.0.jar @@ -164,18 +243,38 @@ javax.servlet-api-3.1.0.jar javax.servlet-api-4.0.1.jar javax.servlet.jsp-2.3.2.jar javax.servlet.jsp-api-2.3.1.jar +javax.ws.rs-api-2.1.jar javolution-5.5.1.jar jaxb-api-2.2.11.jar jaxb-api-2.3.1.jar jaxb-core-3.0.2.jar jaxb-impl-3.0.2.jar +jbcrypt-0.4.jar jboss-logging-3.3.3.Final.jar +jcabi-log-0.14.jar +jcabi-manifests-1.1.jar jcip-annotations-1.0-1.jar +jcl-over-slf4j-1.7.28.jar jcodings-1.0.18.jar jcommander-1.72.jar jcommander-1.78.jar +jcseg-core-2.6.2.jar +jctools-core-2.1.1.jar +jersey-apache-connector-2.27.jar +jersey-client-1.19.jar +jersey-client-2.27.jar +jersey-common-2.27.jar jersey-container-servlet-core-2.25.1.jar jersey-container-servlet-core-2.27.jar +jersey-core-1.19.jar +jersey-guice-1.19.jar +jersey-hk2-2.27.jar +jersey-json-1.19.jar +jersey-server-1.19.jar +jersey-servlet-1.19.jar +jetcd-common-0.5.9.jar +jetcd-core-0.5.9.jar +jettison-1.1.jar jetty-client-9.4.33.v20201020.jar jetty-client-9.4.40.v20210413.jar jetty-http-9.4.19.v20190610.jar @@ -198,14 +297,22 @@ jetty-webapp-9.4.19.v20190610.jar jetty-webapp-9.4.40.v20210413.jar jetty-xml-9.4.19.v20190610.jar jetty-xml-9.4.40.v20210413.jar +jieba-analysis-1.0.2.jar +jjwt-api-0.11.5.jar +jjwt-impl-0.11.5.jar +jjwt-jackson-0.11.5.jar jline-2.12.jar +jline-2.14.6.jar jline-3.9.0.jar +jna-4.5.2.jar joda-time-2.10.3.jar joda-time-2.10.8.jar joni-2.1.11.jar jpam-1.1.jar +jraft-core-1.3.11.jar jsch-0.1.55.jar json-1.8.jar +json-io-2.5.1.jar json-smart-2.3.jar json-smart-2.4.2.jar jsp-api-2.1.jar @@ -213,28 +320,61 @@ jsqlparser-3.1.jar jsr305-3.0.0.jar jsr305-3.0.1.jar jsr305-3.0.2.jar +jsr311-api-1.1.1.jar jul-to-slf4j-1.7.28.jar kerb-admin-1.0.1.jar +kerb-admin-2.0.0.jar kerb-client-1.0.1.jar +kerb-client-2.0.0.jar kerb-common-1.0.1.jar +kerb-common-2.0.0.jar kerb-core-1.0.1.jar kerb-crypto-1.0.1.jar +kerb-crypto-2.0.0.jar kerb-identity-1.0.1.jar +kerb-identity-2.0.0.jar kerb-server-1.0.1.jar +kerb-server-2.0.0.jar kerb-simplekdc-1.0.1.jar +kerb-simplekdc-2.0.0.jar kerb-util-1.0.1.jar +kerb-util-2.0.0.jar kerby-asn1-1.0.1.jar kerby-config-1.0.1.jar +kerby-config-2.0.0.jar kerby-pkix-1.0.1.jar kerby-util-1.0.1.jar kerby-xdr-1.0.1.jar +kerby-xdr-2.0.0.jar kotlin-stdlib-1.6.20.jar kotlin-stdlib-common-1.5.31.jar kotlin-stdlib-jdk7-1.2.71.jar kotlin-stdlib-jdk7-1.6.10.jar kotlin-stdlib-jdk8-1.2.71.jar kotlin-stdlib-jdk8-1.6.10.jar +kubernetes-client-5.6.0.jar +kubernetes-model-admissionregistration-5.6.0.jar +kubernetes-model-apiextensions-5.6.0.jar +kubernetes-model-apps-5.6.0.jar +kubernetes-model-autoscaling-5.6.0.jar +kubernetes-model-batch-5.6.0.jar +kubernetes-model-certificates-5.6.0.jar +kubernetes-model-common-5.6.0.jar +kubernetes-model-coordination-5.6.0.jar +kubernetes-model-core-5.6.0.jar +kubernetes-model-discovery-5.6.0.jar +kubernetes-model-events-5.6.0.jar +kubernetes-model-extensions-5.6.0.jar +kubernetes-model-flowcontrol-5.6.0.jar +kubernetes-model-metrics-5.6.0.jar +kubernetes-model-networking-5.6.0.jar +kubernetes-model-node-5.6.0.jar +kubernetes-model-policy-5.6.0.jar +kubernetes-model-rbac-5.6.0.jar +kubernetes-model-scheduling-5.6.0.jar +kubernetes-model-storageclass-5.6.0.jar leveldbjni-all-1.8.jar +libfb303-0.9.3.jar libthrift-0.9.3.jar lightning-csv-8.2.1.jar listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar @@ -243,15 +383,25 @@ log4j-1.2-api-2.17.1.jar log4j-1.2.17.jar log4j-api-2.18.0.jar log4j-core-2.18.0.jar -log4j-jul-2.11.2.jar log4j-slf4j-impl-2.18.0.jar +log4j-to-slf4j-2.11.2.jar +log4j-web-2.11.2.jar +logback-classic-1.2.3.jar +logback-core-1.2.3.jar logging-interceptor-4.10.0.jar -lombok-1.18.8.jar +lombok-1.18.32.jar +lucene-analyzers-common-8.11.2.jar +lucene-analyzers-smartcn-8.11.2.jar +lucene-core-8.11.2.jar +lucene-queries-4.7.2.jar +lucene-queryparser-4.7.2.jar +lucene-sandbox-4.7.2.jar lz4-java-1.4.0.jar +memory-0.9.0.jar metrics-core-4.2.0.jar metrics-json-4.2.0.jar metrics-jvm-4.2.0.jar -micrometer-core-1.1.6.jar +mmseg4j-core-1.10.0.jar mssql-jdbc-6.4.0.jre8.jar mssql-jdbc-7.2.0.jre8.jar mybatis-3.5.2.jar @@ -267,29 +417,44 @@ netty-3.10.6.Final.jar netty-all-4.1.65.Final.jar netty-buffer-4.1.65.Final.jar netty-codec-4.1.65.Final.jar +netty-codec-http-4.1.65.Final.jar +netty-codec-http2-4.1.65.Final.jar +netty-codec-socks-4.1.65.Final.jar netty-common-4.1.65.Final.jar netty-handler-4.1.65.Final.jar +netty-handler-proxy-4.1.65.Final.jar netty-resolver-4.1.65.Final.jar netty-transport-4.1.65.Final.jar netty-transport-native-epoll-4.1.65.Final.jar netty-transport-native-unix-common-4.1.65.Final.jar nimbus-jose-jwt-9.8.1.jar +nlp-lang-1.7.7.jar +ohc-core-0.7.4.jar okhttp-4.10.0.jar +okhttp-4.12.0.jar +okio-3.6.0.jar okio-jvm-3.0.0.jar +okio-jvm-3.6.0.jar opencsv-2.3.jar orc-core-1.5.8.jar orc-core-1.6.14.jar orc-shims-1.5.8.jar orc-shims-1.6.14.jar +osgi-resource-locator-1.0.1.jar ow2-asm-6.2.jar paranamer-2.3.jar parboiled-core-1.1.8.jar +parboiled-core-1.2.0.jar +parquet-hadoop-bundle-1.10.0.jar perfmark-api-0.23.0.jar +picocli-4.0.1.jar postgresql-42.2.6.jar postgresql-42.4.1.jar proto-google-common-protos-2.0.1.jar protobuf-java-2.5.0.jar protobuf-java-3.17.2.jar +protobuf-java-3.17.3.jar +protobuf-java-util-3.17.2.jar re2j-1.1.jar sfm-converter-8.2.1.jar sfm-csv-8.2.1.jar @@ -297,25 +462,25 @@ sfm-map-8.2.1.jar sfm-reflect-8.2.1.jar sfm-tuples-8.2.1.jar sfm-util-8.2.1.jar +sketches-core-0.9.0.jar slf4j-api-1.7.25.jar slf4j-api-1.7.28.jar slf4j-api-1.7.30.jar slf4j-log4j12-1.7.30.jar +smiley-http-proxy-servlet-1.12.1.jar snakeyaml-1.23.jar snappy-java-1.1.8.2.jar snappy-java-1.1.8.4.jar +sofa-common-tools-1.0.12.jar spring-aop-5.1.9.RELEASE.jar spring-beans-5.1.9.RELEASE.jar spring-boot-2.1.8.RELEASE.jar -spring-boot-actuator-2.1.8.RELEASE.jar -spring-boot-actuator-autoconfigure-2.1.8.RELEASE.jar spring-boot-autoconfigure-2.1.8.RELEASE.jar spring-boot-starter-2.1.8.RELEASE.jar -spring-boot-starter-actuator-2.1.8.RELEASE.jar spring-boot-starter-cache-2.1.8.RELEASE.jar spring-boot-starter-jdbc-2.1.8.RELEASE.jar spring-boot-starter-json-2.1.8.RELEASE.jar -spring-boot-starter-log4j2-2.1.8.RELEASE.jar +spring-boot-starter-logging-2.1.8.RELEASE.jar spring-boot-starter-tomcat-2.1.8.RELEASE.jar spring-boot-starter-web-2.1.8.RELEASE.jar spring-context-5.1.9.RELEASE.jar @@ -330,7 +495,9 @@ spring-webmvc-5.1.9.RELEASE.jar stax-api-1.0.1.jar stax2-api-4.2.1.jar threeten-extra-1.5.0.jar +tinkergraph-gremlin-3.5.1.jar token-provider-1.0.1.jar +token-provider-2.0.0.jar tomcat-embed-core-9.0.24.jar tomcat-embed-el-9.0.24.jar tomcat-embed-websocket-9.0.24.jar @@ -341,5 +508,6 @@ websocket-common-9.4.40.v20210413.jar woodstox-core-5.0.3.jar woodstox-core-5.3.0.jar xz-1.8.jar +zjsonpatch-0.3.0.jar zookeeper-3.6.2.jar zookeeper-jute-3.6.2.jar diff --git a/hugegraph-hubble/Dockerfile b/hugegraph-hubble/Dockerfile index 377b6fb41..4591715d7 100644 --- a/hugegraph-hubble/Dockerfile +++ b/hugegraph-hubble/Dockerfile @@ -40,5 +40,7 @@ FROM eclipse-temurin:11-jre-jammy COPY --from=build /pkg/hugegraph-hubble/apache-hugegraph-hubble-*/ /hubble WORKDIR /hubble/ +# SECURITY: This is a plain HTTP port. Do not publish it to an untrusted network; +# production/public deployments must put Hubble behind HTTPS and block bypasses. EXPOSE 8088 -ENTRYPOINT ["./bin/start-hubble.sh", "-f true"] +ENTRYPOINT ["./bin/start-hubble.sh", "-f"] diff --git a/hugegraph-hubble/README.md b/hugegraph-hubble/README.md index 6b03d5aff..d9196a168 100644 --- a/hugegraph-hubble/README.md +++ b/hugegraph-hubble/README.md @@ -7,6 +7,49 @@ hugegraph-hubble is a graph management and analysis platform that provides features: graph data load, schema management, graph relationship analysis, and graphical display. +## Functional Modules Overview + +```mermaid +graph TD + Hubble["HugeGraph-Hubble Platform"] + + Hubble --> Conn["1. Workspace Management
(Multi-Graph Connections)"] + Hubble --> Schema["2. Visual Schema Designer
(Vertex, Edge & Index Types)"] + Hubble --> Load["3. Guided Data Importer
(Source Mapping & Task Monitor)"] + Hubble --> Analyze["4. Graph Analysis & Visualization
(Gremlin Console & Visual Exploration)"] + Hubble --> System["5. System Administration
(Async Tasks & Access Control)"] +``` + +
+ASCII diagram (for terminals/editors) + +``` + ┌──────────────────────────────────────────┐ + │ HugeGraph-Hubble Platform │ + └────────────────────┬─────────────────────┘ + │ + ┌───────────────────────────┼───────────────────────────┐ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Workspace │ │Visual Schema │ │ Guided Data │ +│ Management │ │ Designer │ │ Importer │ +│ - Connect │ │ - Vertex │ │ - Sources │ +│ - Switch │ │ - Edge │ │ - Mapping │ +│ - Card View │ │ - Index │ │ - Monitor │ +└──────────────┘ └──────────────┘ └──────────────┘ + │ │ + └───────────────────────────┬───────────────────────────┘ + │ + ▼ + ┌──────────────────────────┐ + │Graph Analysis & Explorer │ + │ - Gremlin Console │ + │ - Algorithm Execution │ + │ - Topology Exploration │ + └──────────────────────────┘ +``` +
+ ## Features - Graph connection management, supporting to easily switch graph to operate @@ -16,13 +59,13 @@ graph data load, schema management, graph relationship analysis, and graphical d ## Quick Start -There are three ways to get HugeGraph-Loader: +There are three ways to get HugeGraph-Hubble: - Download the Toolchain binary package - Source code compilation - Use Docker image (Convenient for Test/Dev) -And you can find more details in the [doc](https://hugegraph.apache.org/docs/quickstart/hugegraph-loader/#2-get-hugegraph-loader) +And you can find more details in the [doc](https://hugegraph.apache.org/docs/quickstart/hugegraph-hubble/#2-deploy) ### 1. Download the Toolchain binary package @@ -44,16 +87,16 @@ Then use a web browser to access `ip:8088` and you can see the `Hubble` page. Yo ### 2. Clone source code then compile and install -> Note: Compiling Hubble requires the user’s local environment to have Node.js V16.x and yarn installed. +> Note: Compiling Hubble requires the user's local environment to have Node.js V18.20.8 and yarn installed. ```bash apt install curl build-essential curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash source ~/.bashrc -nvm install 16 +nvm install 18.20.8 ``` -Then, verify that the installed Node.js version is 16.x (please note that higher Node version may cause conflicts). +Then, verify that the installed Node.js version is 18.20.8. ```bash node -v diff --git a/hugegraph-hubble/hubble-be/pom.xml b/hugegraph-hubble/hubble-be/pom.xml index 6e81c1691..ff96b6031 100644 --- a/hugegraph-hubble/hubble-be/pom.xml +++ b/hugegraph-hubble/hubble-be/pom.xml @@ -1,5 +1,6 @@ + + co.elastic.clients + elasticsearch-java + ${es.version} + org.mybatis.spring.boot mybatis-spring-boot-starter - ${mybatis.starter.version} + 2.1.0 com.baomidou mybatis-plus-boot-starter - ${mybatis.plus.starter.version} + 3.3.0 com.h2database @@ -90,12 +96,16 @@ com.github.ben-manes.caffeine caffeine + 2.8.0 + + + net.sourceforge.javacsv + javacsv + 2.0 - org.apache.hugegraph - hugegraph-loader - ${revision} + hugegraph-common org.apache.logging.log4j @@ -117,29 +127,97 @@ javassist org.javassist + + + + org.apache.hugegraph + hg-pd-client + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-slf4j-impl + + + + + org.apache.hugegraph + hugegraph-loader + ${revision} + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-slf4j-impl + org.eclipse.jetty jetty-runner - com.oracle - ojdbc8 + hive-exec + org.apache.hive - slf4j-log4j12 - org.slf4j + com.google.protobuf + protobuf-java + + + + + org.apache.hugegraph + hugegraph-client + ${revision} + + + org.apache.logging.log4j + log4j-api - log4j - log4j + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-slf4j-impl - org.jetbrains.kotlin - kotlin-stdlib + org.eclipse.jetty + jetty-runner - com.squareup.okhttp - okhttp + hive-exec + org.apache.hive + + + org.glassfish.jersey.containers + jersey-container-servlet-core + + + org.glassfish.jersey.core + jersey-common + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.core + jersey-server @@ -147,20 +225,73 @@ commons-fileupload commons-fileupload + 1.4 - - - com.squareup.okhttp3 - okhttp + org.parboiled + parboiled-core + 1.2.0 - org.jetbrains.kotlin - kotlin-stdlib + org.apache.hugegraph + hugegraph-core + compile + + + + org.apache.hugegraph + hg-store-common + + + + + org.apache.commons + commons-collections4 + 4.2 + compile + + + commons-configuration + commons-configuration + 1.10 + + + com.google.protobuf + protobuf-java + 3.17.3 + + + + org.apache.hive + hive-exec + 3.1.3 + core + compile + + + com.google.guava + guava + + + com.google.protobuf + protobuf-java + + + + org.apache.logging.log4j + log4j-slf4j-impl + + + - org.jetbrains.kotlin - kotlin-stdlib-common + com.squareup.okhttp3 + okhttp + 4.12.0 @@ -178,6 +309,70 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + + info + true + + + + + maven-compiler-plugin + 3.8.0 + + ${java.version} + ${java.version} + + -Xmaxerrs + 500 + -Xlint:unchecked + -Xlint:deprecation + --add-modules=java.base + + + + + maven-clean-plugin + 3.0.0 + + + + ${top.level.dir} + + *.tar + *.tar.gz + *.zip + ${final.name}/** + + false + + + ${final.name} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.20 + org.apache.maven.plugins maven-jar-plugin @@ -231,6 +426,7 @@ + diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/HugeGraphHubble.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/HugeGraphHubble.java index e0c208279..dad629fdf 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/HugeGraphHubble.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/HugeGraphHubble.java @@ -21,6 +21,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.util.Ex; import org.mybatis.spring.annotation.MapperScan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -30,13 +32,20 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.ZoneOffset; +import java.util.TimeZone; + @SpringBootApplication @EnableScheduling @MapperScan("org.apache.hugegraph.mapper") public class HugeGraphHubble extends SpringBootServletInitializer { + private static final Logger LOG = LoggerFactory.getLogger(HugeGraphHubble.class); + public static void main(String[] args) { + LOG.info("user.dir ==> {}", System.getProperty("user.dir")); initEnv(); + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.of("+8"))); SpringApplication.run(HugeGraphHubble.class, args); } @@ -53,7 +62,8 @@ public static void initEnv() { } @Override - protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { + protected SpringApplicationBuilder configure( + SpringApplicationBuilder builder) { return builder.sources(this.getClass()); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/AppName.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/AppName.java new file mode 100644 index 000000000..ba0ac39af --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/AppName.java @@ -0,0 +1,53 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.common; + +import org.apache.hugegraph.type.define.SerialEnum; + +public enum AppName implements SerialEnum { + GREMLIN(1, "GREMLIN"), + + VERTEX_UPDATE(2, "VERTEX_UPDATE"), + + EDGE_UPDATE(3, "EDGE_UPDATE") + ; + + private final byte code; + private final String name; + + static { + SerialEnum.register(AppName.class); + } + + AppName(int code, String name) { + assert code < 256; + this.code = (byte) code; + this.name = name; + } + + @Override + public byte code() { + return this.code; + } + + public String string() { + return this.name; + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/AppType.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/AppType.java new file mode 100644 index 000000000..046805201 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/AppType.java @@ -0,0 +1,54 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.common; + +import org.apache.hugegraph.type.define.SerialEnum; + +public enum AppType implements SerialEnum { + // 通用功能 + GENERAL(1, "GENERAL"), + + // 定制功能 + CUSTOMIZED(2, "CUSTOMIZED"), + + ; + + private final byte code; + private final String name; + + static { + SerialEnum.register(AppType.class); + } + + AppType(int code, String name) { + assert code < 256; + this.code = (byte) code; + this.name = name; + } + + @Override + public byte code() { + return this.code; + } + + public String string() { + return this.name; + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java index 11afe6a46..d649513f9 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java @@ -18,13 +18,13 @@ package org.apache.hugegraph.common; -import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.collect.ImmutableSet; import java.nio.charset.Charset; import java.util.Set; import java.util.regex.Pattern; -import com.google.common.collect.ImmutableSet; +import static java.nio.charset.StandardCharsets.UTF_8; public final class Constant { @@ -39,15 +39,13 @@ public final class Constant { public static final String CONFIG_FILE = "hugegraph-hubble.properties"; public static final String CONTROLLER_PACKAGE = - "org.apache.hugegraph.controller"; + "org.apache.hugegraph.controller"; public static final String COOKIE_USER = "user"; public static final String API_V1_1 = "/api/v1.1/"; public static final String API_V1_2 = "/api/v1.2/"; - public static final String API_VERSION = API_V1_2; - - public static final String EDITION_COMMUNITY = "community"; - public static final String EDITION_COMMERCIAL = "commercial"; + public static final String API_V1_3 = "/api/v1.3/"; + public static final String API_VERSION = API_V1_3; public static final String MAPPING_FILE_NAME = "mapping.json"; @@ -58,6 +56,12 @@ public final class Constant { public static final int STATUS_ILLEGAL_GREMLIN = 460; public static final int STATUS_INTERNAL_ERROR = 500; + public static final String TOKEN_KEY = "auth_token"; + public static final String USERNAME_KEY = "username"; + public static final String CREDENTIAL_PASSWORD_KEY = "auth_password"; + public static final String CREDENTIAL_EXPIRES_AT_KEY = "auth_password_expire_at"; + public static final long CREDENTIAL_TTL_MILLIS = 10 * 60 * 1000L; + public static final int NO_LIMIT = -1; public static final Pattern COMMON_NAME_PATTERN = Pattern.compile( @@ -73,4 +77,6 @@ public final class Constant { ); public static final String[] LIKE_WILDCARDS = {"%", "_", "^", "[", "]"}; + + public static final String BUILT_IN = "neizhianli"; } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Mergeable.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Mergeable.java index 3fd638bc3..1d9e3e477 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Mergeable.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Mergeable.java @@ -19,5 +19,4 @@ package org.apache.hugegraph.common; public interface Mergeable { - } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/OptionType.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/OptionType.java new file mode 100644 index 000000000..a1d1e294a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/OptionType.java @@ -0,0 +1,56 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.common; + +import org.apache.hugegraph.type.define.SerialEnum; + +public enum OptionType implements SerialEnum { + + + DELETE(1, "DELETE"), + + ADD(2, "ADD"), + + UPDATE(3, "UPDATE"), + + ; + + private final byte code; + private final String name; + + static { + SerialEnum.register(OptionType.class); + } + + OptionType(int code, String name) { + assert code < 256; + this.code = (byte) code; + this.name = name; + } + + @Override + public byte code() { + return this.code; + } + + public String string() { + return this.name; + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/CacheConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/CacheConfig.java index d41b1e55b..8b367d307 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/CacheConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/CacheConfig.java @@ -43,7 +43,10 @@ public class CacheConfig { public enum Caches { // No used - GREMLIN_QUERY; + GREMLIN_QUERY, + + // es query cached + ES_QUERY(DEFAULT_MAXSIZE, 60); private int maxSize = DEFAULT_MAXSIZE; private int ttl = DEFAULT_TTL; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/DatabaseSchemaMigrator.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/DatabaseSchemaMigrator.java new file mode 100644 index 000000000..13e717bf6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/DatabaseSchemaMigrator.java @@ -0,0 +1,155 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.config; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Locale; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Component +public class DatabaseSchemaMigrator implements ApplicationRunner { + + private static final int FILE_MAPPING_PATH_LENGTH = 2048; + private static final String FILE_MAPPING_TABLE = "file_mapping"; + private static final String FILE_MAPPING_PATH_COLUMN = "path"; + private static final String EXECUTE_HISTORY_TABLE = "execute_history"; + private static final String FAILURE_REASON_COLUMN = "failure_reason"; + + @Autowired + private DataSource dataSource; + + @Override + public void run(ApplicationArguments args) throws Exception { + try (Connection conn = this.dataSource.getConnection()) { + this.migrate(conn); + } + } + + public void migrate(Connection conn) throws SQLException { + int currentLength = this.columnSize(conn, FILE_MAPPING_TABLE, + FILE_MAPPING_PATH_COLUMN); + if (currentLength > 0 && currentLength < FILE_MAPPING_PATH_LENGTH) { + this.migrateFileMappingPath(conn, currentLength); + } + + this.migrateExecuteHistoryFailureReason(conn); + } + + private void migrateFileMappingPath(Connection conn, int currentLength) + throws SQLException { + String sql = this.alterFileMappingPathSql( + conn.getMetaData().getDatabaseProductName()); + if (sql == null) { + log.warn("Skip file_mapping.path migration for unsupported " + + "database product {}", + conn.getMetaData().getDatabaseProductName()); + return; + } + + try (Statement statement = conn.createStatement()) { + statement.execute(sql); + } + log.info("Migrated file_mapping.path from {} to VARCHAR({})", + currentLength, FILE_MAPPING_PATH_LENGTH); + } + + private void migrateExecuteHistoryFailureReason(Connection conn) + throws SQLException { + if (!this.tableExists(conn, EXECUTE_HISTORY_TABLE) || + this.columnSize(conn, EXECUTE_HISTORY_TABLE, + FAILURE_REASON_COLUMN) > 0) { + return; + } + + String product = conn.getMetaData().getDatabaseProductName() + .toLowerCase(Locale.ROOT); + if (!product.contains("h2") && !product.contains("mysql") && + !product.contains("mariadb")) { + log.warn("Skip execute_history.failure_reason migration for " + + "unsupported database product {}", + conn.getMetaData().getDatabaseProductName()); + return; + } + + try (Statement statement = conn.createStatement()) { + statement.execute("ALTER TABLE `execute_history` ADD COLUMN " + + "`failure_reason` VARCHAR(64) DEFAULT NULL"); + } + log.info("Added execute_history.failure_reason VARCHAR(64)"); + } + + private boolean tableExists(Connection conn, String table) + throws SQLException { + DatabaseMetaData metaData = conn.getMetaData(); + String[] tables = {table, table.toUpperCase(Locale.ROOT)}; + for (String tableName : tables) { + try (ResultSet rs = metaData.getTables(null, null, tableName, + new String[]{"TABLE"})) { + if (rs.next()) { + return true; + } + } + } + return false; + } + + private int columnSize(Connection conn, String table, String column) + throws SQLException { + DatabaseMetaData metaData = conn.getMetaData(); + String[] tables = {table, table.toUpperCase(Locale.ROOT)}; + String[] columns = {column, column.toUpperCase(Locale.ROOT)}; + for (String tableName : tables) { + for (String columnName : columns) { + try (ResultSet rs = metaData.getColumns(null, null, tableName, + columnName)) { + if (rs.next()) { + return rs.getInt("COLUMN_SIZE"); + } + } + } + } + return 0; + } + + private String alterFileMappingPathSql(String productName) { + String product = productName.toLowerCase(Locale.ROOT); + if (product.contains("h2")) { + return "ALTER TABLE `file_mapping` ALTER COLUMN `path` " + + "VARCHAR(2048) NOT NULL"; + } + if (product.contains("mysql") || product.contains("mariadb")) { + return "ALTER TABLE `file_mapping` MODIFY COLUMN `path` " + + "VARCHAR(2048) NOT NULL"; + } + return null; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/GlobalCorsConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/GlobalCorsConfig.java index 565f94aad..8706c8de0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/GlobalCorsConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/GlobalCorsConfig.java @@ -30,11 +30,11 @@ public class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); - config.addAllowedOrigin("*"); - config.setAllowCredentials(true); + // Same-origin is the secure default. Deployments that need cross-origin + // access must add an explicit trusted-origin allowlist. + config.setAllowCredentials(false); config.addAllowedMethod("*"); config.addAllowedHeader("*"); - config.addExposedHeader("/api/**"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java index f5e2ae136..8caad2787 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java @@ -18,23 +18,20 @@ package org.apache.hugegraph.config; -import java.io.File; - import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.options.HubbleOptions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.io.File; +import java.net.URL; + @Configuration public class HubbleConfig { - private static final Logger LOG = LoggerFactory.getLogger(HubbleConfig.class); - @Autowired private ApplicationArguments arguments; @@ -43,7 +40,7 @@ public HugeConfig hugeConfig() { String[] args = this.arguments.getSourceArgs(); if (args.length > 1) { throw new ExternalException( - "HugeGraphHubble accept up to one param as config file"); + "HugeGraphHubble accept up to one param as config file"); } else if (args.length == 0) { args = new String[]{Constant.CONFIG_FILE}; } @@ -51,15 +48,13 @@ public HugeConfig hugeConfig() { // Register hubble config options OptionSpace.register(Constant.MODULE_NAME, HubbleOptions.instance()); String conf = args[0]; - try { - String path = HubbleConfig.class.getClassLoader() - .getResource(conf).getPath(); + URL resource = HubbleConfig.class.getClassLoader().getResource(conf); + if (resource != null) { + String path = resource.getPath(); File file = new File(path); if (file.exists() && file.isFile()) { conf = path; } - } catch (Exception ignored) { - LOG.error("hugeConfig exception"); } return new HugeConfig(conf); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/IngestionProxyServlet.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/IngestionProxyServlet.java new file mode 100644 index 000000000..228c3588f --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/IngestionProxyServlet.java @@ -0,0 +1,87 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.config; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.entity.StringEntity; +import org.apache.http.message.BasicHttpResponse; +import org.apache.hugegraph.common.Constant; +import org.mitre.dsmiley.httpproxy.ProxyServlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.io.IOException; + +public class IngestionProxyServlet extends ProxyServlet { + + @Override + protected String rewriteQueryStringFromRequest( + HttpServletRequest servletRequest, String queryString) { + String username = this.sessionString(servletRequest, + Constant.USERNAME_KEY); + + String requestQueryString = servletRequest.getQueryString(); + + if (StringUtils.isEmpty(requestQueryString)) { + requestQueryString = String.format("user=%s", username); + } else { + requestQueryString += String.format("&user=%s", username); + } + + return requestQueryString; + } + + @Override + protected HttpResponse doExecute(HttpServletRequest servletRequest, + HttpServletResponse servletResponse, + HttpRequest proxyRequest) throws IOException { + if (!this.authenticated(servletRequest)) { + // check user login + HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, + Constant.STATUS_UNAUTHORIZED, + "Unauthorized"); + + response.setHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + response.setEntity(new StringEntity("{\"status\": 401}", + StandardCharsets.UTF_8)); + + return response; + } + + return super.doExecute(servletRequest, servletResponse, proxyRequest); + } + + private boolean authenticated(HttpServletRequest servletRequest) { + return StringUtils.isNotBlank(this.sessionString(servletRequest, + Constant.USERNAME_KEY)) && + StringUtils.isNotBlank(this.sessionString(servletRequest, + Constant.TOKEN_KEY)); + } + + private String sessionString(HttpServletRequest servletRequest, + String key) { + Object value = servletRequest.getSession().getAttribute(key); + return value instanceof String ? (String) value : null; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/JacksonConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/JacksonConfig.java index 4fc2ab300..c8ea6603e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/JacksonConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/JacksonConfig.java @@ -18,21 +18,21 @@ package org.apache.hugegraph.config; -import java.io.IOException; -import java.util.Map; - +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.entity.graph.VertexQueryEntity; import org.apache.hugegraph.structure.graph.Edge; import org.apache.hugegraph.structure.graph.Vertex; import org.springframework.boot.jackson.JsonComponent; import org.springframework.context.annotation.Bean; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.util.Map; @JsonComponent public class JacksonConfig { @@ -75,6 +75,11 @@ public void serialize(Vertex vertex, JsonGenerator generator, writeIdField("id", vertex.id(), generator); generator.writeStringField("label", vertex.label()); writePropertiesField(vertex.properties(), generator, provider); + if (vertex instanceof VertexQueryEntity) { + writeStatisticsField( + ((VertexQueryEntity) vertex).getStatistics(), + generator, provider); + } generator.writeEndObject(); } } @@ -98,7 +103,7 @@ public void serialize(Edge edge, JsonGenerator generator, private static void writeIdField(String fieldName, Object id, JsonGenerator generator) - throws IOException { + throws IOException { // Serialize id to string generator.writeStringField(fieldName, id.toString()); } @@ -106,7 +111,7 @@ private static void writeIdField(String fieldName, Object id, private static void writePropertiesField(Map properties, JsonGenerator generator, SerializerProvider provider) - throws IOException { + throws IOException { // Start write properties generator.writeFieldName("properties"); generator.writeStartObject(); @@ -130,4 +135,32 @@ private static void writePropertiesField(Map properties, // End wirte properties generator.writeEndObject(); } + + private static void writeStatisticsField(Map statistics, + JsonGenerator generator, + SerializerProvider provider) + throws IOException { + // Start write statistics + generator.writeFieldName("statistics"); + generator.writeStartObject(); + for (Map.Entry entry : statistics.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + generator.writeFieldName(key); + if (value != null) { + if (value instanceof Long) { + // To avoid javascript loss of long precision + generator.writeString(String.valueOf(value)); + } else { + JsonSerializer serializer; + serializer = provider.findValueSerializer(value.getClass()); + serializer.serialize(value, generator, provider); + } + } else { + generator.writeNull(); + } + } + // End wirte statistics + generator.writeEndObject(); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/MetaConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/MetaConfig.java new file mode 100644 index 000000000..2bf5370a6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/MetaConfig.java @@ -0,0 +1,60 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.config; + +import org.apache.hugegraph.driver.factory.PDHugeClientFactory; +import org.apache.hugegraph.options.HubbleOptions; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Configuration +public class MetaConfig { + + @Autowired + private HugeConfig config; + + @Bean("pdEnabled") + public Boolean pdEnabled() { + return this.config.get(HubbleOptions.PD_ENABLED); + } + + @Bean("cluster") + public String getCluster() { + return this.config.get(HubbleOptions.PD_CLUSTER); + } + + @Bean + PDHugeClientFactory pdHugeClientFactory() { + boolean pdEnabled = this.config.get(HubbleOptions.PD_ENABLED); + if (!pdEnabled) { + log.info("PD mode is disabled, skip creating PDHugeClientFactory"); + return null; + } + + String pdAddrs = this.config.get(HubbleOptions.PD_PEERS); + String routeType = this.config.get(HubbleOptions.ROUTE_TYPE); + return new PDHugeClientFactory(pdAddrs, routeType); + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/ProxyServletConfiguration.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/ProxyServletConfiguration.java new file mode 100644 index 000000000..e3f5a7162 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/ProxyServletConfiguration.java @@ -0,0 +1,62 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.config; + +import org.apache.hugegraph.options.HubbleOptions; + +import org.mitre.dsmiley.httpproxy.ProxyServlet; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ProxyServletConfiguration { + @Autowired + private HugeConfig config; + + /** + * Only register the proxy servlet when proxy.servlet_url is configured. + * When not configured, this bean won't be created, preventing the proxy + * servlet from intercepting all requests on root path. + */ + @Bean + @ConditionalOnProperty(name = "proxy.servlet_url", matchIfMissing = false) + public ServletRegistrationBean + servletRegistrationBean() { + String servletUrl = config.get(HubbleOptions.PROXY_SERVLET_URL); + String targetUrl = config.get(HubbleOptions.PROXY_TARGET_URL); + + // Additional safety check + if (StringUtils.isBlank(servletUrl) || + StringUtils.isBlank(targetUrl) || + "WhatURLAtHere".equals(targetUrl)) { + return null; + } + + ServletRegistrationBean servletRegistrationBean = + new ServletRegistrationBean<>(new IngestionProxyServlet(), + servletUrl); + servletRegistrationBean.addInitParameter("targetUri", targetUrl); + servletRegistrationBean.addInitParameter(ProxyServlet.P_LOG, "true"); + return servletRegistrationBean; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java index 9d0de09b1..f19e86695 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java @@ -18,9 +18,6 @@ package org.apache.hugegraph.config; -import java.net.InetAddress; -import java.net.UnknownHostException; - import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.options.HubbleOptions; import org.apache.tomcat.util.http.LegacyCookieProcessor; @@ -29,10 +26,15 @@ import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.stereotype.Component; -// TODO: remove this class if we don't need it anymore +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Reference http://www.zizhixiaoshe.com/article/invalidcookie.html + */ @Component -public class TomcatServletConfig implements - WebServerFactoryCustomizer { +public class TomcatServletConfig + implements WebServerFactoryCustomizer { @Autowired private HugeConfig config; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java index 25ebcf6f5..ec0fb77e8 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java @@ -18,30 +18,65 @@ package org.apache.hugegraph.config; -import org.apache.hugegraph.handler.CustomInterceptor; +import org.apache.hugegraph.handler.LoginInterceptor; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.PathResourceResolver; + +import org.apache.hugegraph.handler.CustomInterceptor; + +import java.io.IOException; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override - public void addViewControllers(ViewControllerRegistry registry) { - registry.addViewController("/{spring:[\\w-]+}") - .setViewName("forward:/"); - registry.addViewController("/**/{spring:[\\w-]+}") - .setViewName("forward:/"); + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/api/**"); + registry.addResourceHandler("/**") + .addResourceLocations("classpath:/ui/") + .resourceChain(true) + .addResolver(new PathResourceResolver() { + @Override + protected Resource getResource(String resourcePath, + Resource location) + throws IOException { + Resource requested = location.createRelative( + resourcePath); + // If the requested resource exists and is readable, + // serve it; otherwise fall back to index.html + // for SPA routing + if (requested.exists() && requested.isReadable()) { + return requested; + } + return new ClassPathResource("/ui/index.html"); + } + }); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(this.customInterceptor()) - .addPathPatterns("/**"); + .addPathPatterns("/api/**"); + registry.addInterceptor(this.loginInterceptor()) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/**/auth/login") + .excludePathPatterns("/logout") + .excludePathPatterns("/api/**/auth/logout"); } + @Bean public CustomInterceptor customInterceptor() { return new CustomInterceptor(); } + + @Bean + public LoginInterceptor loginInterceptor() { + return new LoginInterceptor(); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/AboutController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/AboutController.java index 044268b8d..57418ed02 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/AboutController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/AboutController.java @@ -18,9 +18,6 @@ package org.apache.hugegraph.controller; -import java.util.LinkedHashMap; -import java.util.Map; - import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.handler.MessageSourceHandler; import org.springframework.beans.factory.annotation.Autowired; @@ -28,9 +25,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.LinkedHashMap; +import java.util.Map; +//import org.apache.hugegraph.license.LicenseVerifier; // TODO C Remove Licence + @RestController @RequestMapping("about") -// TODO: delete the class or keep it? public class AboutController extends BaseController { @Autowired @@ -38,11 +38,20 @@ public class AboutController extends BaseController { @GetMapping public Map about() { + //LicenseVerifier verifier = LicenseVerifier.instance();// TODO C Remove Licence Map about = new LinkedHashMap<>(); about.put("name", Constant.SERVER_NAME); - about.put("version", "1.5.0"); - about.put("allowed_datasize", - this.messageHandler.getMessage("license.datasize.no-limit")); + about.put("version", "3.0.0"); + //about.put("edition", verifier.edition());// TODO C Remove Licence + //about.put("allowed_graphs", verifier.allowedGraphs()); + //long datasize = verifier.allowedDataSize(); + //if (datasize == -1) { + // about.put("allowed_datasize", + // this.messageHandler.getMessage("license.datasize.no-limit")); + //} else { + // about.put("allowed_datasize", + // FileUtils.byteCountToDisplaySize(datasize * Bytes.MB)); + //} return about; } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java index bdcd4cd2a..a5767e92c 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java @@ -19,42 +19,280 @@ package org.apache.hugegraph.controller; import java.util.List; +import java.util.function.Function; +import javax.servlet.http.HttpServletRequest; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.factory.PDHugeClientFactory; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.commons.collections.CollectionUtils; +import org.apache.hugegraph.config.HugeConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.exception.ParameterizedException; +import org.apache.hugegraph.service.HugeClientPoolService; import org.apache.hugegraph.common.Identifiable; import org.apache.hugegraph.common.Mergeable; import org.apache.hugegraph.util.EntityUtil; import org.apache.hugegraph.util.Ex; -import org.springframework.util.StringUtils; +@Component public abstract class BaseController { + @Autowired + protected String cluster; + @Autowired + protected HugeClientPoolService hugeClientPoolService; + @Autowired(required = false) + protected PDHugeClientFactory pdHugeClientFactory; + + @Autowired + private HugeConfig config; + + @Autowired + UserService userService; + public static final String ORDER_ASC = "asc"; public static final String ORDER_DESC = "desc"; public void checkIdSameAsBody(Object id, Identifiable newEntity) { Ex.check(newEntity.getId() != null, () -> id.equals(newEntity.getId()), - "common.param.path-id-should-same-as-body", - id, newEntity.getId()); + "common.param.path-id-should-same-as-body", + id, newEntity.getId()); } public void checkParamsNotEmpty(String name, String value, - boolean creating) { + boolean creating) { if (creating) { Ex.check(!StringUtils.isEmpty(value), - "common.param.cannot-be-null-or-empty", name); + "common.param.cannot-be-null-or-empty", name); } else { // The default null and user-passed null indicate no update Ex.check(value == null || !value.isEmpty(), - "common.param.cannot-be-empty", name); + "common.param.cannot-be-empty", name); } } public void checkParamsNotEmpty(String name, List values) { Ex.check(values != null && !values.isEmpty(), - "common.param.cannot-be-null-or-empty", name); + "common.param.cannot-be-null-or-empty", name); } public T mergeEntity(T oldEntity, T newEntity) { return EntityUtil.merge(oldEntity, newEntity); } + + protected void setSession(String key, Object value) { + HttpServletRequest request = getRequest(); + request.getSession().setAttribute(key, value); + } + + protected Object getSession(String key) { + HttpServletRequest request = getRequest(); + return request.getSession().getAttribute(key); + + } + + protected String getUser() { + return (String) getSession(Constant.USERNAME_KEY); + } + + protected void setUser(String username) { + setSession(Constant.USERNAME_KEY, username); + } + + protected void setCredentialPassword(String password) { + // TODO: Stop retaining the plaintext login password after Vermeer migrates to + // token/service credentials and Loader/Ingest token-only paths are verified. + setSession(Constant.CREDENTIAL_PASSWORD_KEY, password); + setSession(Constant.CREDENTIAL_EXPIRES_AT_KEY, + System.currentTimeMillis() + Constant.CREDENTIAL_TTL_MILLIS); + } + + protected String getCredentialPassword() { + Long expiresAt = (Long) getSession(Constant.CREDENTIAL_EXPIRES_AT_KEY); + if (expiresAt == null || expiresAt <= System.currentTimeMillis()) { + delSession(Constant.CREDENTIAL_PASSWORD_KEY); + delSession(Constant.CREDENTIAL_EXPIRES_AT_KEY); + return null; + } + return (String) getSession(Constant.CREDENTIAL_PASSWORD_KEY); + } + + protected void delSession(String key) { + HttpServletRequest request = getRequest(); + request.getSession().removeAttribute(key); + } + + protected HttpServletRequest getRequest() { + return ((ServletRequestAttributes) RequestContextHolder + .getRequestAttributes()).getRequest(); + } + + protected String getToken() { + return (String) getSession(Constant.TOKEN_KEY); + } + + protected void setToken(String token) { + this.setSession(Constant.TOKEN_KEY, token); + } + + protected void delToken() { + this.delSession(Constant.TOKEN_KEY); + } + + protected void clearAuthSession() { + this.delSession(Constant.TOKEN_KEY); + this.delSession(Constant.USERNAME_KEY); + this.delSession(Constant.CREDENTIAL_PASSWORD_KEY); + this.delSession(Constant.CREDENTIAL_EXPIRES_AT_KEY); + } + + protected HugeClient authClient(String graphSpace, String graph) { + HttpServletRequest request = getRequest(); + if (request.getAttribute("hugeClient") != null) { + HugeClient client = (HugeClient) request.getAttribute("hugeClient"); + client.assignGraph(graphSpace, graph); + return client; + } + HugeClient client = this.hugeClientPoolService.createAuthClient( + graphSpace, graph, this.getToken()); + request.setAttribute("hugeClient", client); + return client; + } + + protected HugeClient authGremlinClient(String graphSpace, String graph) { + String username = this.getUser(); + String password = this.getCredentialPassword(); + if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { + return this.authClient(graphSpace, graph); + } + + HttpServletRequest request = getRequest(); + if (request.getAttribute("hugeClient") != null) { + HugeClient client = (HugeClient) request.getAttribute("hugeClient"); + client.close(); + } + HugeClient client = this.createBasicClient(graphSpace, graph, username, + password); + request.setAttribute("hugeClient", client); + return client; + } + + protected HugeClient createBasicClient(String graphSpace, String graph, + String username, String password) { + return this.hugeClientPoolService.createBasicClient(graphSpace, graph, + username, password); + } + + protected HugeClient unauthClient() { + HttpServletRequest request = getRequest(); + if (request.getAttribute("hugeClient") != null) { + HugeClient client = (HugeClient) request.getAttribute("hugeClient"); + return client; + } + HugeClient client = this.hugeClientPoolService.createUnauthClient(); + request.setAttribute("hugeClient", client); + return client; + } + + protected HugeClient tempTokenClient() { + HttpServletRequest request = getRequest(); + if (request.getAttribute("hugeClient") != null) { + HugeClient client = (HugeClient) request.getAttribute("hugeClient"); + client.setAuthContext("Basic " + this.getToken()); + return client; + } + HugeClient client = this.hugeClientPoolService.createTempTokenClient(this.getToken()); + request.setAttribute("hugeClient", client); + return client; + } + + protected void clearRequestHugeClient() { + HttpServletRequest request = getRequest(); + if (request.getAttribute("hugeClient") != null) { + HugeClient client = (HugeClient) request.getAttribute("hugeClient"); + client.close(); + } + request.setAttribute("hugeClient", null); + } + + protected HugeClient createAuthClient(String graphSpace, String graph) { + return this.hugeClientPoolService.create(null, graphSpace, graph, + this.getToken()); + } + + protected HugeClient createUnauthClient(String graphSpace, String graph) { + return this.hugeClientPoolService.create(null, graphSpace, graph, null); + } + + public T doAuthRequest(Function func) { + try (HugeClient client = createAuthClient(null, null)) { + return func.apply(client); + } catch (Throwable t) { + throw t; + } + } + + public T doUnauthRequest(Function func) { + try (HugeClient client = createUnauthClient(null, null)) { + return func.apply(client); + } catch (Throwable t) { + throw t; + } + } + + protected HugeClient defaultClient(String graphSpace, String graph) { + boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED); + if (!pdEnabled) { + String url = config.get(HubbleOptions.SERVER_URL); + return hugeClientPoolService.create(url, graphSpace, graph, + this.getToken()); + } + + // PD mode: get URL from service discovery + List urls = pdHugeClientFactory.getURLs(this.cluster, + PDHugeClientFactory.DEFAULT_GRAPHSPACE, + PDHugeClientFactory.DEFAULT_SERVICE); + + if (CollectionUtils.isEmpty(urls)) { + throw new ParameterizedException("No url in service(%s/%s)", + PDHugeClientFactory.DEFAULT_GRAPHSPACE, + PDHugeClientFactory.DEFAULT_SERVICE); + } + + String url = urls.get((int) (Math.random() * urls.size())); + + HugeClient client = hugeClientPoolService.create(url, graphSpace, graph, + this.getToken()); + + return client; + } + + public String getUrl() { + boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED); + if (!pdEnabled) { + return config.get(HubbleOptions.SERVER_URL); + } + + List urls = pdHugeClientFactory.getURLs(this.cluster, + PDHugeClientFactory.DEFAULT_GRAPHSPACE, + PDHugeClientFactory.DEFAULT_SERVICE); + + if (CollectionUtils.isEmpty(urls)) { + throw new ParameterizedException("No url in service(%s/%s)", + PDHugeClientFactory.DEFAULT_GRAPHSPACE, + PDHugeClientFactory.DEFAULT_SERVICE); + } + + String url = urls.get((int) (Math.random() * urls.size())); + return url; + } + } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java new file mode 100644 index 000000000..5b3795d8a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java @@ -0,0 +1,49 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.options.HubbleOptions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constant.API_VERSION + "config") +public class ConfigController { + + @Autowired + private HugeConfig config; + + @GetMapping + public Map getConfig() { + Map result = new HashMap<>(); + boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED); + result.put("pd_enabled", pdEnabled); + if (!pdEnabled) { + result.put("server_url", config.get(HubbleOptions.SERVER_URL)); + } + return result; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/GraphConnectionController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/GraphConnectionController.java deleted file mode 100644 index 4dfa07a52..000000000 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/GraphConnectionController.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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 org.apache.hugegraph.controller; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.List; -import java.util.regex.Pattern; - -import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.common.Constant; -import org.apache.hugegraph.common.Response; -import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.driver.HugeClient; -import org.apache.hugegraph.entity.GraphConnection; -import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.options.HubbleOptions; -import org.apache.hugegraph.service.GraphConnectionService; -import org.apache.hugegraph.service.HugeClientPoolService; -import org.apache.hugegraph.service.SettingSSLService; -import org.apache.hugegraph.util.Ex; -import org.apache.hugegraph.util.HubbleUtil; -import org.apache.hugegraph.util.HugeClientUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.baomidou.mybatisplus.core.metadata.IPage; - -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@RestController -@RequestMapping(Constant.API_VERSION + "graph-connections") -public class GraphConnectionController extends BaseController { - - private static final Pattern GRAPH_PATTERN = Pattern.compile( - "^[A-Za-z][A-Za-z0-9_]{0,47}$" - ); - - @Autowired - private HugeConfig config; - @Autowired - private GraphConnectionService connService; - @Autowired - private HugeClientPoolService poolService; - @Autowired - private SettingSSLService sslService; - - @GetMapping - public Response list(@RequestParam(name = "content", required = false) - String content, - @RequestParam(name = "page_no", required = false, - defaultValue = "1") - int pageNo, - @RequestParam(name = "page_size", required = false, - defaultValue = "10") - int pageSize) { - IPage conns = this.connService.list(content, pageNo, - pageSize); - return Response.builder().status(Constant.STATUS_OK).data(conns) - .build(); - } - - @GetMapping("{id}") - public GraphConnection get(@PathVariable("id") int id) { - GraphConnection entity = this.connService.get(id); - if (entity == null) { - throw new ExternalException("graph-connection.not-exist.id", id); - } - if (!this.poolService.containsKey(id)) { - this.sslService.configSSL(this.config, entity); - HugeClient client = HugeClientUtil.tryConnect(entity); - this.poolService.put(entity, client); - } - return entity; - } - - @PostMapping - public GraphConnection create(@RequestBody GraphConnection newEntity) { - this.checkParamsValid(newEntity, true); - this.checkAddressSecurity(newEntity); - // Make sure the new entity doesn't conflict with exists - this.checkEntityUnique(newEntity, true); - - newEntity.setTimeout(this.config.get(HubbleOptions.CLIENT_REQUEST_TIMEOUT)); - // Do connect test, failure will throw an exception - this.sslService.configSSL(this.config, newEntity); - HugeClient client = HugeClientUtil.tryConnect(newEntity); - newEntity.setCreateTime(HubbleUtil.nowDate()); - - this.connService.save(newEntity); - this.poolService.put(newEntity, client); - return newEntity; - } - - @PutMapping("{id}") - public GraphConnection update(@PathVariable("id") int id, - @RequestBody GraphConnection newEntity) { - this.checkIdSameAsBody(id, newEntity); - this.checkParamsValid(newEntity, false); - this.checkAddressSecurity(newEntity); - - // Check exist connection with this id - GraphConnection oldEntity = this.connService.get(id); - if (oldEntity == null) { - throw new ExternalException("graph-connection.not-exist.id", id); - } - GraphConnection entity = this.mergeEntity(oldEntity, newEntity); - // Make sure the updated connection doesn't conflict with exists - this.checkEntityUnique(entity, false); - this.sslService.configSSL(this.config, entity); - HugeClient client = HugeClientUtil.tryConnect(entity); - - this.connService.update(entity); - this.poolService.put(entity, client); - return entity; - } - - @DeleteMapping("{id}") - public GraphConnection delete(@PathVariable("id") int id) { - GraphConnection oldEntity = this.connService.get(id); - if (oldEntity == null) { - throw new ExternalException("graph-connection.not-exist.id", id); - } - this.connService.remove(id); - this.poolService.remove(oldEntity); - return oldEntity; - } - - private void checkParamsValid(GraphConnection newEntity, boolean creating) { - Ex.check(creating, () -> newEntity.getId() == null, - "common.param.must-be-null", "id"); - - String name = newEntity.getName(); - this.checkParamsNotEmpty("name", name, creating); - Ex.check(name != null, () -> Constant.COMMON_NAME_PATTERN.matcher(name) - .matches(), - "graph-connection.name.unmatch-regex"); - - String graph = newEntity.getGraph(); - this.checkParamsNotEmpty("graph", graph, creating); - Ex.check(graph != null, () -> GRAPH_PATTERN.matcher(graph).matches(), - "graph-connection.graph.unmatch-regex"); - - String host = newEntity.getHost(); - this.checkParamsNotEmpty("host", host, creating); - Ex.check(host != null, () -> HubbleUtil.HOST_PATTERN.matcher(host) - .matches(), - "graph-connection.host.unmatch-regex"); - - Integer port = newEntity.getPort(); - Ex.check(creating, () -> port != null, - "common.param.cannot-be-null", "port"); - Ex.check(port != null, () -> 0 < port && port <= 65535, - "graph-connection.port.must-be-in-range", "[1, 65535]", port); - - Ex.check((StringUtils.isEmpty(newEntity.getUsername()) && - StringUtils.isEmpty(newEntity.getPassword())) || - (!StringUtils.isEmpty(newEntity.getUsername()) && - !StringUtils.isEmpty(newEntity.getPassword())), - "graph-connection.username-or-password.must-be-same-status"); - - Ex.check(newEntity.getCreateTime() == null, - "common.param.must-be-null", "create_time"); - } - - private void checkAddressSecurity(GraphConnection newEntity) { - String host = newEntity.getHost(); - Integer port = newEntity.getPort(); - InetAddress address; - try { - address = InetAddress.getByName(host); - } catch (UnknownHostException e) { - throw new ExternalException("graph-connection.host.unresolved"); - } - String ip = address.getHostAddress(); - log.debug("The host: {}, ip: {}", address.getHostName(), ip); - - List ipWhiteList = this.config.get( - HubbleOptions.CONNECTION_IP_WHITE_LIST); - if (!ipWhiteList.contains("*")) { - Ex.check(ipWhiteList.contains(host) || ipWhiteList.contains(ip), - "graph-connection.host.unauthorized"); - } - - List portWhiteList = this.config.get( - HubbleOptions.CONNECTION_PORT_WHITE_LIST); - if (!portWhiteList.contains(-1)) { - Ex.check(portWhiteList.contains(port), - "graph-connection.port.unauthorized"); - } - } - - private void checkEntityUnique(GraphConnection newEntity, - boolean creating) { - List oldEntities = this.connService.listAll(); - for (GraphConnection oldEntity : oldEntities) { - // NOTE: create should check all, update check others - if (!creating && oldEntity.getId().equals(newEntity.getId())) { - continue; - } - Ex.check(!oldEntity.getName().equals(newEntity.getName()), - "graph-connection.exist.name", oldEntity.getName()); - Ex.check(!(oldEntity.getGraph().equals(newEntity.getGraph()) && - oldEntity.getHost().equals(newEntity.getHost()) && - oldEntity.getPort().equals(newEntity.getPort())), - "graph-connection.exist.graph-host-port", - oldEntity.getGraph(), oldEntity.getHost(), - oldEntity.getPort()); - } - } -} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java index 1a00a1afa..4f9a746cb 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java @@ -18,12 +18,6 @@ package org.apache.hugegraph.controller; -import java.util.concurrent.TimeUnit; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.entity.UserInfo; import org.apache.hugegraph.service.UserInfoService; @@ -34,6 +28,11 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeUnit; + @RestController @RequestMapping(Constant.API_VERSION + "setting") public class SettingController { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OlapAlgoController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OlapAlgoController.java new file mode 100644 index 000000000..e20e716f9 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OlapAlgoController.java @@ -0,0 +1,50 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.algorithm; + +import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.algorithm.OlapEntity; +import org.apache.hugegraph.entity.query.OlapView; +import org.apache.hugegraph.service.algorithm.OlapAlgoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Log4j2 +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/algorithms/olap") +public class OlapAlgoController extends BaseController { + @Autowired + private OlapAlgoService service; + + @PostMapping + public OlapView olapView(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestBody OlapEntity body) { + HugeClient client = this.authClient(graphspace, graph); + return this.service.olapView(client, graphspace, body); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java index 28a265968..c50d77ab0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java @@ -18,10 +18,46 @@ package org.apache.hugegraph.controller.algorithm; +import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.api.traverser.NeighborRankAPI; +import org.apache.hugegraph.api.traverser.PersonalRankAPI; +// TODO fix import +//import org.apache.hugegraph.client.api.traverser.NeighborRankAPI; +//import org.apache.hugegraph.client.api.traverser.PersonalRankAPI; import org.apache.hugegraph.common.Constant; -import org.apache.hugegraph.entity.algorithm.ShortestPath; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.algorithm.AdamicadarEntity; +import org.apache.hugegraph.entity.algorithm.AllShortestPathsEntity; +import org.apache.hugegraph.entity.algorithm.CrossPointsEntity; +import org.apache.hugegraph.entity.algorithm.JaccardSimilarityEntity; +import org.apache.hugegraph.entity.algorithm.KneighborEntity; +import org.apache.hugegraph.entity.algorithm.KoutEntity; +import org.apache.hugegraph.entity.algorithm.PathsEntity; +import org.apache.hugegraph.entity.algorithm.RaysEntity; +import org.apache.hugegraph.entity.algorithm.ResourceallocationEntity; +import org.apache.hugegraph.entity.algorithm.RingsEntity; +import org.apache.hugegraph.entity.algorithm.SameNeighborsEntity; +import org.apache.hugegraph.entity.algorithm.ShortestPathEntity; +import org.apache.hugegraph.entity.algorithm.SingleSourceShortestPathEntity; +import org.apache.hugegraph.entity.algorithm.WeightedShortestPathEntity; +import org.apache.hugegraph.entity.query.EgonetView; +import org.apache.hugegraph.entity.query.FusiformsimilarityView; import org.apache.hugegraph.entity.query.GremlinResult; +import org.apache.hugegraph.entity.query.JaccardsimilarityView; +import org.apache.hugegraph.entity.query.RanksView; import org.apache.hugegraph.service.algorithm.OltpAlgoService; +import org.apache.hugegraph.structure.traverser.CrosspointsRequest; +import org.apache.hugegraph.structure.traverser.CustomizedPathsRequest; +import org.apache.hugegraph.structure.traverser.EgonetRequest; +import org.apache.hugegraph.structure.traverser.FusiformSimilarityRequest; +import org.apache.hugegraph.structure.traverser.KneighborRequest; +import org.apache.hugegraph.structure.traverser.KoutRequest; +import org.apache.hugegraph.structure.traverser.MultiNodeShortestPathRequest; +import org.apache.hugegraph.structure.traverser.PathsRequest; +import org.apache.hugegraph.structure.traverser.SameNeighborsBatchRequest; +import org.apache.hugegraph.structure.traverser.SingleSourceJaccardSimilarityRequest; +import org.apache.hugegraph.structure.traverser.TemplatePathsRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -29,19 +65,246 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import lombok.extern.log4j.Log4j2; +import java.util.Map; @Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/algorithms") -public class OltpAlgoController { +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/algorithms/oltp") +public class OltpAlgoController extends BaseController { @Autowired private OltpAlgoService service; @PostMapping("shortestPath") - public GremlinResult shortPath(@PathVariable("connId") int connId, - @RequestBody ShortestPath body) { - return this.service.shortestPath(connId, body); + public GremlinResult shortPath(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ShortestPathEntity body) { + HugeClient client = this.authGremlinClient(graphSpace, graph); + return this.service.shortestPath(client, body); + } + + @PostMapping("shortpath") + public GremlinResult shortPathAlias(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ShortestPathEntity body) { + return this.shortPath(graphSpace, graph, body); + } + + @PostMapping("rings") + public GremlinResult rings(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody RingsEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.rings(client, body); + } + + @PostMapping("advancedPaths") + public GremlinResult advancedPaths(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody PathsRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.advancedpaths(client, body); + } + + @PostMapping("sameNeighbors") + public GremlinResult sameNeighbors(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody SameNeighborsEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.sameNeighbors(client, body); + } + + @PostMapping("kout") + public GremlinResult kout(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody KoutEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.kout(client, body); + } + + @PostMapping("kout_post") + public GremlinResult koutPost(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody KoutRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.koutPost(client, body); + } + + @PostMapping("kneighbor") + public GremlinResult kneighbor(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody KneighborEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.kneighbor(client, body); + } + + @PostMapping("kneighbor_post") + public GremlinResult kneighborPost(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody KneighborRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.kneighborPost(client, body); + } + + @PostMapping("jaccardSimilarity") + public JaccardsimilarityView jaccardSimilarity(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody JaccardSimilarityEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.jaccardSimilarity(client, body); + } + + @PostMapping("jaccardSimilarity_post") + public JaccardsimilarityView jaccardSimilarityPost( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody SingleSourceJaccardSimilarityRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.jaccardSimilarityPost(client, body); + } + + @PostMapping("personalrank") + public RanksView personalRank(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody PersonalRankAPI.Request body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.personalRank(client, body); + } + + @PostMapping("neighborrank") + public RanksView neighborRank(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody NeighborRankAPI.Request body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.neighborRank(client, body); + } + + @PostMapping("allshortestpaths") + public GremlinResult allShortPaths(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody AllShortestPathsEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.allShortestPaths(client, body); + } + + @PostMapping("allshortpath") + public GremlinResult allShortPathAlias(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody AllShortestPathsEntity body) { + return this.allShortPaths(graphSpace, graph, body); + } + + @PostMapping("weightedshortestpath") + public GremlinResult weightedShortestPath(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody WeightedShortestPathEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.weightedShortestPath(client, body); + } + + @PostMapping("singlesourceshortestpath") + public GremlinResult singleSourceShortestPath( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody SingleSourceShortestPathEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.singleSourceShortestPath(client, body); + } + + @PostMapping("multinodeshortestpath") + public GremlinResult multiNodeShortestPath(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody MultiNodeShortestPathRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.multiNodeShortestPath(client, body); + } + + @PostMapping("paths") + public GremlinResult paths(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody PathsEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.paths(client, body); + } + + @PostMapping("customizedpaths") + public GremlinResult customizedPaths(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody CustomizedPathsRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.customizedPaths(client, body); + } + + @PostMapping("templatepaths") + public GremlinResult templatePaths(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody TemplatePathsRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.templatePaths(client, body); + } + + @PostMapping("crosspoints") + public GremlinResult crosspoints(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody CrossPointsEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.crosspoints(client, body); + } + + @PostMapping("customizedcrosspoints") + public GremlinResult customizedcrosspoints(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody CrosspointsRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.customizedcrosspoints(client, body); + } + + @PostMapping("rays") + public GremlinResult rays(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody RaysEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.rays(client, body); + } + + @PostMapping("fusiformsimilarity") + public FusiformsimilarityView fusiformsimilarity(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody FusiformSimilarityRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.fusiformsimilarity(client, body); + } + + @PostMapping("adamicadar") + public Map adamicadar(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody AdamicadarEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.adamicadar(client, body); + } + + @PostMapping("resourceallocation") + public Map resourceallocation(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ResourceallocationEntity body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.resourceallocation(client, body); + } + + @PostMapping("sameneighborsbatch") + public GremlinResult sameneighborsbatch(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody SameNeighborsBatchRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.sameneighborsbatch(client, body); + } + + @PostMapping("egonet") + public EgonetView egonet(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody EgonetRequest body) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.egonet(client, body); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/VermeerAlgoController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/VermeerAlgoController.java new file mode 100644 index 000000000..c9eca817c --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/VermeerAlgoController.java @@ -0,0 +1,92 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.loader.util.JsonUtil; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.space.VermeerService; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.HubbleUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/algorithms/vermeer") +public class VermeerAlgoController extends BaseController { + + @Autowired + VermeerService vermeerService; + @Autowired + private HugeConfig config; + + @PostMapping + public Map olapView(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestBody VParams body) { + String vGraph = vermeerService.convert2VG(graphspace, graph); + HugeClient client = this.authClient(null, null); + + Map graphInfo = HubbleUtil.uncheckedCast( + client.vermeer().getGraphInfoByName(vGraph).get("graph")); + E.checkArgument(graphInfo != null && !graphInfo.isEmpty(), + "graph not loaded"); + + Map params = new HashMap<>(); + // default params + String pdPeers = config.get(HubbleOptions.PD_PEERS); + String pdJson = JsonUtil.toJson(Arrays.asList(pdPeers.split(","))); + params.put("output.parallel", "10"); + params.put("output.type", "hugegraph"); + params.put("output.hg_pd_peers", pdJson); + params.put("output.hugegraph_name", graphspace + "/" + graph + "/g"); + params.put("output.hugegraph_username", this.getUser()); + params.put("output.hugegraph_password", this.getCredentialPassword()); + params.put("output.hugegraph_property", body.params.get("compute" + + ".algorithm")); + // input params + params.putAll(body.analyze()); + return vermeerService.compute(client, graphspace, graph, params); + } + + private static class VParams { + @JsonProperty("params") + public Map params; + + public Map analyze() { + for (Map.Entry entry : params.entrySet()) { + entry.setValue(entry.getValue().toString()); + } + return params; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AccessController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AccessController.java new file mode 100644 index 000000000..dbd723be6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AccessController.java @@ -0,0 +1,82 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import java.util.List; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.AccessEntity; +import org.apache.hugegraph.service.auth.AccessService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/auth/accesses") +public class AccessController extends AuthController { + + @Autowired + private AccessService accessService; + + @GetMapping + public List list( + @PathVariable("graphspace") String graphSpace, + @RequestParam(value = "role_id", required = false) String roleId, + @RequestParam(value = "target_id", required = false) String targetId) { + HugeClient client = this.authClient(graphSpace, null); + return this.accessService.list(client, graphSpace, roleId, targetId); + } + + @GetMapping("{id}") + public AccessEntity get(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String accessId) { + HugeClient client = this.authClient(graphSpace, null); + return this.accessService.get(client, graphSpace, accessId); + } + + @PostMapping + public AccessEntity add(@PathVariable("graphspace") String graphSpace, + @RequestBody AccessEntity accessEntity) { + HugeClient client = this.authClient(graphSpace, null); + return this.accessService.addOrUpdate(client, graphSpace, accessEntity); + } + + @PutMapping + public AccessEntity update(@PathVariable("graphspace") String graphSpace, + @RequestBody AccessEntity accessEntity) { + HugeClient client = this.authClient(graphSpace, null); + return this.accessService.addOrUpdate(client, graphSpace, accessEntity); + } + + @DeleteMapping + public void delete(@PathVariable("graphspace") String graphSpace, + @RequestParam("role_id") String roleId, + @RequestParam("target_id") String targetId) { + HugeClient client = this.authClient(graphSpace, null); + this.accessService.delete(client, roleId, targetId); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AuthController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AuthController.java new file mode 100644 index 000000000..cf6ebb6f5 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AuthController.java @@ -0,0 +1,24 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import org.apache.hugegraph.controller.BaseController; + +public class AuthController extends BaseController { +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/BelongController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/BelongController.java new file mode 100644 index 000000000..16b0b9201 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/BelongController.java @@ -0,0 +1,122 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import java.util.ArrayList; +import java.util.List; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.BelongEntity; +import org.apache.hugegraph.service.auth.BelongService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/auth/belongs") +public class BelongController extends AuthController { + + @Autowired + private BelongService belongService; + + public List list( + @PathVariable("graphspace") String graphSpace, + @RequestParam(value = "role_id", required = false) String roleId, + @RequestParam(value = "user_id", required = false) String userId) { + HugeClient client = this.authClient(graphSpace, null); + return this.belongService.list(client, roleId, userId); + } + + @GetMapping + public IPage listPage( + @PathVariable("graphspace") String graphSpace, + @RequestParam(value = "role_id", required = false) String roleId, + @RequestParam(value = "user_id", required = false) String userId, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphSpace, null); + return this.belongService.listPage(client, roleId, userId, pageNo, + pageSize); + } + + @GetMapping("{id}") + public BelongEntity get(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String belongId) { + HugeClient client = this.authClient(graphSpace, null); + return this.belongService.get(client, belongId); + } + + @PostMapping + public void create(@PathVariable("graphspace") String graphSpace, + @RequestBody BelongEntity belongEntity) { + HugeClient client = this.authClient(graphSpace, null); + this.belongService.add(client, belongEntity.getRoleId(), + belongEntity.getUserId()); + } + + @PostMapping("ids") + public void createMany(@PathVariable("graphspace") String graphSpace, + @RequestBody BelongService.BelongsReq belongsReq) { + HugeClient client = this.authClient(graphSpace, null); + for (String userId : belongsReq.getUserIds()) { + this.belongService.add(client, belongsReq.getRoleId(), userId); + } + } + + @DeleteMapping("{id}") + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String belongId) { + HugeClient client = this.authClient(graphSpace, null); + this.belongService.delete(client, belongId); + } + + @DeleteMapping + public void delete(@PathVariable("graphspace") String graphSpace, + @RequestParam("role_id") String roleId, + @RequestParam("user_id") String userId) { + HugeClient client = this.authClient(graphSpace, null); + if (StringUtils.isNotEmpty(roleId) && StringUtils.isNotEmpty(userId)) { + this.belongService.delete(client, roleId, userId); + } + } + + @PostMapping("delids") + public void deleteMany(@PathVariable("graphspace") String graphSpace, + @RequestBody DelIdsReq delIdsReq) { + HugeClient client = this.authClient(graphSpace, null); + this.belongService.deleteMany(client, + delIdsReq.ids.toArray(new String[0])); + } + + public static class DelIdsReq { + + public List ids = new ArrayList<>(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java new file mode 100644 index 000000000..4464bc2f8 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java @@ -0,0 +1,118 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.UserView; +import org.apache.hugegraph.service.auth.GraphSpaceUserService; +import org.apache.hugegraph.structure.auth.User; +import org.apache.hugegraph.structure.auth.UserManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/auth/users") +public class GraphSpaceUserController extends AuthController { + + @Autowired + private GraphSpaceUserService userService; + + @GetMapping + public IPage querySpaceUserViews( + @PathVariable("graphspace") String graphSpace, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphSpace, null); + return this.userService.queryPage(client, query, pageNo, pageSize); + } + + @GetMapping("spaceadmin") + public IPage querySpaceAdmins( + @PathVariable("graphspace") String graphSpace, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphSpace, null); + return this.userService.querySpaceAdmins(client, graphSpace, query, + pageNo, pageSize); + } + + @GetMapping("{id}") + public UserView get(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String userId) { + HugeClient client = this.authClient(graphSpace, null); + return this.userService.getUser(client, userId); + } + + @GetMapping("spaceadmin/{id}") + public UserManager setGraphSpaceAdmin( + @PathVariable("graphspace") String graphSpace, + @PathVariable("id") String userId) { + HugeClient client = this.authClient(null, null); + return client.auth().addSpaceAdmin(userId, graphSpace); + } + + @DeleteMapping("spaceadmin/{id}") + public void removeGraphSpaceAdmin( + @PathVariable("graphspace") String graphSpace, + @PathVariable("id") String userId) { + HugeClient client = this.authClient(null, null); + client.auth().delSpaceAdmin(userId, graphSpace); + } + + @PostMapping + public UserView create(@PathVariable("graphspace") String graphSpace, + @RequestBody UserView userView) { + HugeClient client = this.authClient(graphSpace, null); + return this.userService.createOrUpdate(client, userView); + } + + @PutMapping("{id}") + public UserView createOrUpdate(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String userId, + @RequestBody UserView userView) { + HugeClient client = this.authClient(graphSpace, null); + userView.setId(userId); + return this.userService.createOrUpdate(client, userView); + } + + @DeleteMapping("{id}") + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String userId) { + HugeClient client = this.authClient(graphSpace, null); + this.userService.unauthUser(client, userId); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GroupController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GroupController.java new file mode 100644 index 000000000..f2fc8ad03 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GroupController.java @@ -0,0 +1,102 @@ +///* +// * +// * Licensed to the Apache Software Foundation (ASF) under one or more +// * contributor license agreements. See the NOTICE file distributed with this +// * work for additional information regarding copyright ownership. The ASF +// * licenses this file to You 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 org.apache.hugegraph.controller.auth; +// +//import java.util.List; +//import java.util.Map; +// +//import org.apache.hugegraph.controller.BaseController; +//import com.baomidou.mybatisplus.core.metadata.IPage; +//import org.apache.hugegraph.service.auth.GroupService; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.web.bind.annotation.DeleteMapping; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.PathVariable; +//import org.springframework.web.bind.annotation.PostMapping; +//import org.springframework.web.bind.annotation.PutMapping; +//import org.springframework.web.bind.annotation.RequestBody; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RequestParam; +//import org.springframework.web.bind.annotation.RestController; +// +//import org.apache.hugegraph.common.Constant; +//import org.apache.hugegraph.structure.auth.Group; +//import org.apache.hugegraph.driver.HugeClient; +// +//@RestController +//@RequestMapping(Constant.API_VERSION + "auth/groups") +//public class GroupController extends BaseController { +// +// @Autowired +// private GroupService groupService; +// +// @GetMapping("list") +// public List list() { +// HugeClient client = this.authClient(null, null); +// return this.groupService.list(client); +// } +// +// @GetMapping +// public IPage queryPage(@RequestParam(name = "query", required = false, +// defaultValue = "") String query, +// @RequestParam(name = "page_no", required = false, +// defaultValue = "1") int pageNo, +// @RequestParam(name = "page_size", required = false, +// defaultValue = "10") int pageSize) { +// HugeClient client = this.authClient(null, null); +// return this.groupService.queryPage(client, query, pageNo, pageSize); +// } +// +// @GetMapping("{id}") +// public Group get(@PathVariable("id") String rid) { +// HugeClient client = this.authClient(null, null); +// return this.groupService.get(client, rid); +// } +// +// @PostMapping +// public Group add(@RequestBody Group group) { +// HugeClient client = this.authClient(null, null); +// return this.groupService.insert(client, group); +// } +// +// @PutMapping("{id}") +// public Group update(@PathVariable("id") String id, +// @RequestBody Group group) { +// HugeClient client = this.authClient(null, null); +// Group g = this.groupService.get(client, id); +// g.description(group.description()); +// g.nickname(group.nickname()); +// this.groupService.update(client, g); +// +// return g; +// } +// +// @DeleteMapping("{id}") +// public void delete(@PathVariable("id") String id) { +// HugeClient client = this.authClient(null, null); +// this.groupService.delete(client, id); +// } +// +// @PostMapping("batch/{id}") +// public Map batch(@PathVariable("id") String id, +// @RequestBody Map action) { +// HugeClient client = this.authClient(null, null); +// return groupService.batch(client, id, action); +// } +//} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java new file mode 100644 index 000000000..967a78f12 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java @@ -0,0 +1,240 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.entity.auth.UserEntity; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.exception.ServerException; +import com.google.common.collect.ImmutableMap; +import org.apache.hugegraph.driver.factory.PDHugeClientFactory; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.service.auth.LoginAttemptGuard; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.structure.auth.Login; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.structure.auth.LoginResult; + +@RestController +@RequestMapping(Constant.API_VERSION + "auth") +public class LoginController extends BaseController { + + private static final int TOKEN_EXPIRE_SECONDS = 60 * 60 * 24 * 30; + private static final int CONNECT_TIMEOUT_MILLIS = 5000; + private static final int READ_TIMEOUT_MILLIS = 30000; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Autowired + UserService userService; + @Autowired + private HugeConfig config; + @Autowired + private LoginAttemptGuard loginAttemptGuard; + + @PostMapping("/login") + public Object login(@RequestBody Login login) { + String address = this.getRequest().getRemoteAddr(); + boolean pdEnabled = this.config.get(HubbleOptions.PD_ENABLED); + this.loginAttemptGuard.checkAllowed(login.name(), address); + // Set Expire: 1 Month + login.expire(TOKEN_EXPIRE_SECONDS); + try { + LoginResult result = this.authenticate(login, pdEnabled, address); + Object user; + if (!pdEnabled) { + user = currentUser(login.name()); + } else { + try (HugeClient client = + this.createLoginTokenClient(result.token())) { + client.assignGraph(PDHugeClientFactory.DEFAULT_GRAPHSPACE, + null); + UserEntity entity = this.userService.getUser(client, + login.name()); + entity.setSuperadmin( + this.userService.isSuperAdmin(client)); + user = entity; + } + } + + this.getRequest().getSession(); + this.getRequest().changeSessionId(); + this.setUser(login.name()); + this.setCredentialPassword(login.password()); + this.setToken(result.token()); + return user; + } catch (Throwable e) { + this.clearAuthSession(); + throw e; + } finally { + this.clearRequestHugeClient(); + } + } + + private LoginResult authenticate(Login login, boolean pdEnabled, + String address) { + try { + LoginResult result; + if (!pdEnabled) { + result = this.loginStandalone(login); + } else { + try (HugeClient client = + this.createLoginClient(login.name(), + login.password())) { + result = client.auth().login(login); + } + } + this.loginAttemptGuard.reset(login.name(), address); + return result; + } catch (Throwable e) { + if (isAuthenticationFailure(e)) { + this.loginAttemptGuard.recordFailure(login.name(), address); + } else { + this.loginAttemptGuard.release(login.name(), address); + } + throw e; + } + } + + private static boolean isAuthenticationFailure(Throwable error) { + if (error instanceof ServerException) { + int status = ((ServerException) error).status(); + return status == HttpStatus.UNAUTHORIZED.value() || + status == HttpStatus.FORBIDDEN.value(); + } + return error instanceof ExternalException && + (((ExternalException) error).status() == + HttpStatus.UNAUTHORIZED.value() || + ((ExternalException) error).status() == + HttpStatus.FORBIDDEN.value()); + } + + protected LoginResult loginStandalone(Login login) { + String endpoint = this.config.get(HubbleOptions.SERVER_URL) + + "/auth/login"; + HttpURLConnection connection = null; + try { + connection = this.openConnection(new URL(endpoint)); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setConnectTimeout(CONNECT_TIMEOUT_MILLIS); + connection.setReadTimeout(READ_TIMEOUT_MILLIS); + connection.setRequestProperty("Content-Type", + "application/json;charset=UTF-8"); + String auth = login.name() + ":" + login.password(); + String basic = Base64.getEncoder().encodeToString( + auth.getBytes(StandardCharsets.UTF_8)); + connection.setRequestProperty("Authorization", "Basic " + basic); + Map body = ImmutableMap.of( + "user_name", login.name(), + "user_password", login.password(), + "token_expire", TOKEN_EXPIRE_SECONDS + ); + try (OutputStream output = connection.getOutputStream()) { + output.write(MAPPER.writeValueAsBytes(body)); + } + int status = connection.getResponseCode(); + if (status == HttpStatus.UNAUTHORIZED.value() || + status == HttpStatus.FORBIDDEN.value()) { + throw new ExternalException(status, + "graph-connection.username-or-password.incorrect"); + } + if (status >= 400) { + throw new IOException("Standalone login failed: HTTP " + + status); + } + Map response; + try (InputStream input = connection.getInputStream()) { + response = MAPPER.readValue(input, Map.class); + } + LoginResult result = new LoginResult(); + result.token(String.valueOf(response.get("token"))); + return result; + } catch (IOException e) { + throw new RuntimeException("Failed to login HugeGraph Server", e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + protected HttpURLConnection openConnection(URL endpoint) + throws IOException { + return (HttpURLConnection) endpoint.openConnection(); + } + + protected HugeClient createLoginClient(String username, String password) { + return this.hugeClientPoolService.createTempBasicClient(username, + password); + } + + protected HugeClient createLoginTokenClient(String token) { + return this.hugeClientPoolService.createTempTokenClient(token); + } + + private static UserEntity currentUser(String username) { + UserEntity user = new UserEntity(); + user.setId(username); + user.setName(username); + user.setNickname(username); + user.setAdminSpaces(Collections.emptyList()); + user.setResSpaces(Collections.emptyList()); + user.setSpacenum(0); + user.setSuperadmin(false); + return user; + } + + @GetMapping("/status") + public Object status() { + + HugeClient client = authClient(null, null); + + String level = userService.userLevel(client); + + return ImmutableMap.of("level", level); + } + + // FIXME: Change logout to POST and add CSRF/Origin protection after coordinating + // the API change with the frontend; explicitly harden the session cookie as well. + @GetMapping("/logout") + public void logout() { + this.clearAuthSession(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/RoleController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/RoleController.java new file mode 100644 index 000000000..386f1809b --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/RoleController.java @@ -0,0 +1,119 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import java.util.List; +import java.util.Map; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.service.auth.RoleService; +import org.apache.hugegraph.structure.auth.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/auth/roles") +public class RoleController extends AuthController { + + @Autowired + private RoleService roleService; + + @GetMapping("list") + public List listName(@PathVariable("graphspace") String graphSpace) { + HugeClient client = this.authClient(graphSpace, null); + return this.roleService.list(client, graphSpace); + } + + @GetMapping + public IPage queryPage( + @PathVariable("graphspace") String graphSpace, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphSpace, null); + return this.roleService.queryPage(client, graphSpace, query, pageNo, + pageSize); + } + + @GetMapping("{id}") + public Role get(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String roleId) { + HugeClient client = this.authClient(graphSpace, null); + return this.roleService.get(client, graphSpace, roleId); + } + + @PostMapping + public Role add(@PathVariable("graphspace") String graphSpace, + @RequestBody Role role) { + HugeClient client = this.authClient(graphSpace, null); + role.graphSpace(graphSpace); + return this.roleService.insert(client, role); + } + + @PutMapping("{id}") + public Role update(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String id, + @RequestBody Map body) { + HugeClient client = this.authClient(graphSpace, null); + Role current = this.roleService.get(client, graphSpace, id); + String name = firstNonBlank(body, "role_name", "group_name", + "new_group_name"); + if (name != null) { + current.name(name); + current.nickname(name); + } + String description = firstNonBlank(body, "role_description", + "group_description"); + if (description != null) { + current.description(description); + } + return this.roleService.update(client, current); + } + + @DeleteMapping("{id}") + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String id) { + HugeClient client = this.authClient(graphSpace, null); + this.roleService.delete(client, id); + } + + private static String firstNonBlank(Map body, + String... keys) { + for (String key : keys) { + Object value = body.get(key); + if (value instanceof String && !((String) value).isEmpty()) { + return (String) value; + } + } + return null; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/TargetController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/TargetController.java new file mode 100644 index 000000000..5dbda66bf --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/TargetController.java @@ -0,0 +1,96 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import java.util.List; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.service.auth.TargetService; +import org.apache.hugegraph.structure.auth.Target; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/auth/targets") +public class TargetController extends AuthController { + + @Autowired + private TargetService targetService; + + @GetMapping("list") + public List list(@PathVariable("graphspace") String graphSpace) { + HugeClient client = this.authClient(graphSpace, null); + return this.targetService.list(client); + } + + @GetMapping + public IPage queryPage( + @PathVariable("graphspace") String graphSpace, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphSpace, null); + return this.targetService.queryPage(client, query, pageNo, pageSize); + } + + @GetMapping("{id}") + public Target get(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String targetId) { + HugeClient client = this.authClient(graphSpace, null); + return this.targetService.get(client, targetId); + } + + @PostMapping + public Target add(@PathVariable("graphspace") String graphSpace, + @RequestBody Target target) { + HugeClient client = this.authClient(graphSpace, null); + return this.targetService.add(client, target); + } + + @PutMapping("{id}") + public Target update(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String targetId, + @RequestBody Target target) { + HugeClient client = this.authClient(graphSpace, null); + Target current = this.targetService.get(client, targetId); + current.resources(target.resources()); + current.description(target.description()); + return this.targetService.update(client, current); + } + + @DeleteMapping("{id}") + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("id") String targetId) { + HugeClient client = this.authClient(graphSpace, null); + this.targetService.delete(client, targetId); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java new file mode 100644 index 000000000..8c69bc33a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java @@ -0,0 +1,159 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import com.google.common.collect.ImmutableMap; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.PasswordEntity; +import org.apache.hugegraph.entity.auth.UserEntity; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.exception.UnauthorizedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RestController +@RequestMapping(Constant.API_VERSION + "auth/users") +public class UserController extends BaseController { + + @Autowired + UserService userService; + + @GetMapping("list") + public Object list() { + List users = this.userService.listUsers( + this.authClient(null, null)); + return ImmutableMap.of("users", users); + } + + @GetMapping + public Object queryPage(@RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + return userService.queryPage(this.authClient(null, null), + query, pageNo, pageSize); + } + + @PostMapping + public void create(@RequestBody UserEntity userEntity) { + HugeClient client = this.authClient(null, null); + userService.add(client, userEntity); + } + + @PostMapping("batch") + public void createbatch(@RequestParam("file") MultipartFile csvfile) { + HugeClient client = this.authClient(null, null); + userService.addbatch(client, csvfile); + } + + @GetMapping("{id}") + public Object get(@PathVariable("id") String id) { + return userService.get(this.authClient(null, null), + id); + } + + @PutMapping("{id}") + public void update(@PathVariable("id") String id, + @RequestBody UserEntity userEntity) { + userEntity.setId(id); + userService.update(this.authClient(null, null), userEntity); + } + + @DeleteMapping("{id}") + public void delete(@PathVariable("id") String id) { + userService.delete(this.authClient(null, null), id); + } + + @PostMapping("updatepwd") + public Response updatepwd(@RequestBody PasswordEntity pwd) { + HugeClient client = this.authClient(null, null); + return userService.updatepwd(client, pwd.getUsername(), pwd.getOldpwd(), pwd.getNewpwd()); + } + + @GetMapping("listadminspace/{username}") + public List listadminspace(@PathVariable("username") String username) { + HugeClient client = this.authClient(null, null); + return userService.listAdminSpace(client, username); + } + + @PostMapping("updateadminspace/{username}") + public void updateadminspace(@PathVariable("username") String username, + @RequestBody List adminspaces) { + HugeClient client = this.authClient(null, null); + userService.updateAdminSpace(client, username, adminspaces); + } + + @PutMapping("personal") + public void updatePersonal(@RequestBody PersonalProfile profile) { + String username = this.getUser(); + if (username == null) { + throw new UnauthorizedException(); + } + this.checkParamsNotEmpty("nickname", profile.getNickname(), true); + String description = profile.getDescription() == null ? "" : + profile.getDescription(); + userService.updatePersonal(this.authClient(null, null), username, + profile.getNickname(), description); + } + + @GetMapping("getpersonal") + public Object getpersonal() { + return userService.getpersonal(this.authClient(null, null), + getUser()); + } + + public static class PersonalProfile { + + private String nickname; + private String description; + + public String getNickname() { + return this.nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/WhiteIpListController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/WhiteIpListController.java new file mode 100644 index 000000000..99300acdb --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/WhiteIpListController.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.auth; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.WhiteIpListEntity; +import org.apache.hugegraph.service.auth.WhiteIpListService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping(Constant.API_VERSION + "auth/whiteiplist") +public class WhiteIpListController extends AuthController { + @Autowired + private WhiteIpListService whiteListService; + + @GetMapping("list") + public Map list() { + HugeClient client = this.authClient(null, null); + return this.whiteListService.get(client); + } + + @PostMapping("batch") + public Map update(@RequestBody WhiteIpListEntity whiteIpListEntity) { + HugeClient client = this.authClient(null, null); + return this.whiteListService.batch(client, whiteIpListEntity); + } + + @PutMapping("updatestatus") + public Map update(@RequestBody boolean status) { + HugeClient client = this.authClient(null, null); + return this.whiteListService.updatestatus(client, status); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graph/GraphController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graph/GraphController.java index d103059fd..1acb0d5b9 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graph/GraphController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graph/GraphController.java @@ -18,35 +18,51 @@ package org.apache.hugegraph.controller.graph; -import java.util.Map; -import java.util.Set; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; -import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.common.Constant; -import org.apache.hugegraph.controller.BaseController; -import org.apache.hugegraph.entity.graph.EdgeEntity; -import org.apache.hugegraph.entity.graph.VertexEntity; -import org.apache.hugegraph.entity.query.GraphView; -import org.apache.hugegraph.entity.schema.EdgeLabelEntity; -import org.apache.hugegraph.entity.schema.VertexLabelEntity; -import org.apache.hugegraph.service.graph.GraphService; -import org.apache.hugegraph.service.schema.EdgeLabelService; -import org.apache.hugegraph.service.schema.VertexLabelService; -import org.apache.hugegraph.structure.constant.IdStrategy; +import org.apache.hugegraph.common.OptionType; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.loader.util.JsonUtil; import org.apache.hugegraph.structure.graph.Edge; import org.apache.hugegraph.structure.graph.Vertex; -import org.apache.hugegraph.util.Ex; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.util.UriUtils; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.entity.graph.EdgeEntity; +import org.apache.hugegraph.entity.graph.VertexEntity; +import org.apache.hugegraph.entity.graph.VertexQueryEntity; +import org.apache.hugegraph.entity.query.ElementEditHistory; +import org.apache.hugegraph.entity.query.GraphView; +import org.apache.hugegraph.service.graph.GraphService; +import org.apache.hugegraph.service.query.EditElementHistoryService; +import org.apache.hugegraph.service.schema.EdgeLabelService; +import org.apache.hugegraph.service.schema.VertexLabelService; +import org.apache.hugegraph.util.E; + +import lombok.extern.slf4j.Slf4j; + @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/graph") +@Slf4j +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}") public class GraphController extends BaseController { @Autowired @@ -55,84 +71,209 @@ public class GraphController extends BaseController { private EdgeLabelService elService; @Autowired private GraphService graphService; + @Autowired + private EditElementHistoryService editEleHisService; @PostMapping("vertex") - public GraphView addVertex(@PathVariable("connId") int connId, + public GraphView addVertex(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody VertexEntity entity) { - this.checkParamsValid(connId, entity, true); - return this.graphService.addVertex(connId, entity); + HugeClient client = this.authClient(graphSpace, graph); + GraphView graphView = this.graphService.addVertex(client, entity); + + E.checkState(graphView.getVertices().size() == 1, + "Adding a vertex must return exactly one vertex"); + VertexQueryEntity v = graphView.getVertices().iterator().next(); + String vertexId = v.id().toString(); + addEditEleHistory(graphSpace, graph, vertexId, entity.getLabel(), + v.properties().size(), + OptionType.ADD, JsonUtil.toJson(entity)); + return graphView; } @PutMapping("vertex/{id}") - public Vertex updateVertex(@PathVariable("connId") int connId, + public Vertex updateVertex(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") String vertexId, @RequestBody VertexEntity entity) { + HugeClient client = this.authClient(graphSpace, graph); vertexId = UriUtils.decode(vertexId, Constant.CHARSET); - this.checkParamsValid(connId, entity, false); this.checkIdSameAsBody(vertexId, entity); - return this.graphService.updateVertex(connId, entity); + Vertex result = + this.graphService.updateVertex(client, vertexId, entity); + addEditEleHistory(graphSpace, graph, vertexId, + entity.getLabel(), result.properties().size(), + OptionType.UPDATE, JsonUtil.toJson(entity)); + return result; + } + + @DeleteMapping("vertex/{id}") + public void deleteVertex(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("id") String vertexId) { + HugeClient client = this.authClient(graphSpace, graph); + vertexId = UriUtils.decode(vertexId, Constant.CHARSET); + Vertex vertex = client.graph().getVertex(vertexId); + String label = vertex.label(); + + this.graphService.deleteVertex(client, vertexId); + addEditEleHistory(graphSpace, graph, vertexId, label, + vertex.properties().size(), OptionType.DELETE, ""); } @PostMapping("edge") - public GraphView addEdge(@PathVariable("connId") int connId, + public GraphView addEdge(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody EdgeEntity entity) { - this.checkParamsValid(connId, entity, true); - return this.graphService.addEdge(connId, entity); + HugeClient client = this.authClient(graphSpace, graph); + GraphView edge = this.graphService.addEdge(client, entity); + E.checkState(edge.getEdges().size() == 1, + "Adding an edge must return exactly one edge"); + Edge e = edge.getEdges().iterator().next(); + String edgeId = e.id().toString(); + addEditEleHistory(graphSpace, graph, + edgeId, entity.getLabel(), + e.properties().size(), OptionType.ADD, + JsonUtil.toJson(entity)); + return edge; } @PutMapping("edge/{id}") - public Edge updateEdge(@PathVariable("connId") int connId, + public Edge updateEdge(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") String edgeId, @RequestBody EdgeEntity entity) { edgeId = UriUtils.decode(edgeId, Constant.CHARSET); - this.checkParamsValid(connId, entity, false); + HugeClient client = this.authClient(graphSpace, graph); this.checkIdSameAsBody(edgeId, entity); - return this.graphService.updateEdge(connId, entity); + Edge edge = this.graphService.updateEdge(client, edgeId, entity); + addEditEleHistory(graphSpace, graph, edgeId, entity.getLabel(), + edge.properties().size(), OptionType.UPDATE, + JsonUtil.toJson(entity)); + return edge; } - private void checkParamsValid(int connId, VertexEntity entity, - boolean create) { - Ex.check(!StringUtils.isEmpty(entity.getLabel()), - "common.param.cannot-be-null-or-empty", "label"); - // If schema doesn't exist, it will throw exception - VertexLabelEntity vlEntity = this.vlService.get(entity.getLabel(), - connId); - IdStrategy idStrategy = vlEntity.getIdStrategy(); - if (create) { - Ex.check(idStrategy.isCustomize(), () -> entity.getId() != null, - "common.param.cannot-be-null", "id"); - } else { - Ex.check(entity.getId() != null, - "common.param.cannot-be-null", "id"); - } + @DeleteMapping("edge/{id}") + public void deleteEdge(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("id") String edgeId) { + edgeId = UriUtils.decode(edgeId, Constant.CHARSET); + HugeClient client = this.authClient(graphSpace, graph); + Edge edge = client.graph().getEdge(edgeId); + String label = edge.label(); - Set nonNullableProps = vlEntity.getNonNullableProps(); - Map properties = entity.getProperties(); - Ex.check(properties.keySet().containsAll(nonNullableProps), - "graph.vertex.all-nonnullable-prop.should-be-setted"); + this.graphService.deleteEdge(client, edgeId); + addEditEleHistory(graphSpace, graph, edgeId, label, + edge.properties().size(), OptionType.DELETE, ""); } - private void checkParamsValid(int connId, EdgeEntity entity, - boolean create) { - Ex.check(!StringUtils.isEmpty(entity.getLabel()), - "common.param.cannot-be-null-or-empty", "label"); - // If schema doesn't exist, it will throw exception - EdgeLabelEntity elEntity = this.elService.get(entity.getLabel(), connId); - if (create) { - Ex.check(entity.getId() == null, - "common.param.must-be-null", "id"); + @DeleteMapping("element/batch") + public void deleteElements(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "type", + required = true) String type, + @RequestParam(name = "ids", required = true) + List elementIds) { + HugeClient client = this.authClient(graphSpace, graph); + ArrayList list = new ArrayList<>(); + HashSet set = new HashSet<>(elementIds); + if ("VERTEX".equals(type)) { + for (String vertexId : set) { + vertexId = UriUtils.decode(vertexId, Constant.CHARSET); + Vertex vertex = client.graph().getVertex(vertexId); + String label = vertex.label(); + this.graphService.deleteVertex(client, vertexId); + list.add(getEditEleHistory(graphSpace, graph, vertexId, label, + vertex.properties().size(), OptionType.DELETE, "")); + } + } else if ("EDGE".equals(type)) { + for (String edgeId : elementIds) { + edgeId = UriUtils.decode(edgeId, Constant.CHARSET); + Edge edge = client.graph().getEdge(edgeId); + String label = edge.label(); + this.graphService.deleteEdge(client, edgeId); + list.add(getEditEleHistory(graphSpace, graph, edgeId, label, + edge.properties().size(), + OptionType.DELETE, "")); + } } else { - Ex.check(entity.getId() != null, - "common.param.cannot-be-null", "id"); + throw new IllegalArgumentException( + "type must in [VERTEX, EDGE], but got '" + type + "'"); } - Ex.check(entity.getSourceId() != null, - "common.param.must-be-null", "source_id"); - Ex.check(entity.getTargetId() != null, - "common.param.must-be-null", "target_id"); - - Set nonNullableProps = elEntity.getNonNullableProps(); - Map properties = entity.getProperties(); - Ex.check(properties.keySet().containsAll(nonNullableProps), - "graph.edge.all-nonnullable-prop.should-be-setted"); + editEleHisService.add(list); + } + + @GetMapping("edgelabel/{label}") + public HashMap getEdgeProperties( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("label") String label) { + HugeClient client = this.authClient(graphSpace, graph); + return this.graphService.getEdgeProperties(client, label); + } + + @GetMapping("vertexlabel/{label}") + public HashMap getVertexProperties( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("label") String label) { + HugeClient client = this.authClient(graphSpace, graph); + return this.graphService.getVertexProperties(client, label); + } + + @PostMapping("edgestype") + public HashMap getEdgeStyle(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody List labels) { + HugeClient client = this.authClient(graphSpace, graph); + return this.graphService.getEdgeStyle(client, labels); + } + + @PostMapping("vertexstyle") + public HashMap getVertexStyle(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody List labels) { + HugeClient client = this.authClient(graphSpace, graph); + return this.graphService.getVertexStyle(client, labels); + } + + @PostMapping("import") + public GraphView importJson(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam("file") MultipartFile jsonFile) throws IOException { + HugeClient client = this.authClient(graphSpace, graph); + return this.graphService.importJson(client, jsonFile); + } + + private void addEditEleHistory(String graphSpace, + String graph, + String elementId, + String label, + int propertyNum, + OptionType optionType, + String content) { + this.editEleHisService.add(graphSpace, graph, elementId, label, + propertyNum, optionType.name(), + new Date(), getUser(), content); + } + + private ElementEditHistory getEditEleHistory(String graphSpace, + String graph, + String elementId, + String label, + int propertyNum, + OptionType optionType, + String content) { + return ElementEditHistory.builder() + .graphspace(graphSpace) + .graph(graph) + .elementId(elementId) + .label(label) + .propertyNum(propertyNum) + .optionType(optionType.name()) + .optionTime(new Date()) + .optionPerson(getUser()) + .content(content) + .build(); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java new file mode 100644 index 000000000..f61936a2a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java @@ -0,0 +1,359 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.graphs; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.entity.graphs.GraphCloneEntity; +import org.apache.hugegraph.entity.graphs.GraphCreateEntity; +import org.apache.hugegraph.entity.graphs.GraphUpdateEntity; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.entity.graphs.GraphStatisticsEntity; +import org.apache.hugegraph.exception.ServerException; +import org.apache.hugegraph.service.space.VermeerService; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.structure.constant.GraphReadMode; +import com.google.common.collect.ImmutableMap; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.service.graphs.GraphsService; +import org.apache.hugegraph.service.load.JobManagerService; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import org.apache.hugegraph.service.query.GremlinCollectionService; +import org.apache.hugegraph.service.space.SchemaTemplateService; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs") +public class GraphsController extends BaseController { + + @Autowired + GraphsService graphsService; + @Autowired + SchemaTemplateService schemaTemplateService; + @Autowired + ExecuteHistoryService executeHistoryService; + @Autowired + GremlinCollectionService gremlinCollectionService; + @Autowired + JobManagerService jobManagerService; + @Autowired + VermeerService vermeerService; + @Autowired + HugeConfig config; + + public boolean isVermeerEnabled() { + String username = this.getUser(); + String password = this.getCredentialPassword(); + return vermeerService.isVermeerEnabled(username, password); + } + + public String getGraphFromVermeer(String vermeer) { + return vermeer.split("-")[1]; + } + + private Map getVermeerGraphs(String graphspace, + boolean enable) { + if (enable) { + HugeClient client = this.authClient(graphspace, null); + List> graphinfos; + try { + graphinfos = HubbleUtil.uncheckedCast( + client.vermeer().getGraphsInfo().get("graphs")); + } catch (ServerException e) { + log.info("Failed to connect to the Vermeer service", + e.cause()); + graphinfos = null; + } + if (graphinfos == null || graphinfos.size() == 0) { + // if vermeer data cleared + return ImmutableMap.of(); + } + String prefix = graphspace + "-"; + graphinfos = graphinfos.stream().filter((g) -> g.get("name") + .toString().startsWith(prefix)) + .collect(Collectors.toList()); + Map briefs = new HashMap<>(graphinfos.size()); + for (Map info: graphinfos) { + String name = getGraphFromVermeer(info.get("name").toString()); + Map brief = new HashMap<>(); + brief.put("name", name); + brief.put("status", info.get("status").toString()); + + String lastLoadTime = info.get("update_time").toString(); + // todo date format + brief.put("last_load_time", lastLoadTime); + briefs.put(name, brief); + } + return briefs; + } + return ImmutableMap.of(); + } + + private Map getVermeerGraph(String graphspace, + String graph) { + boolean enable = isVermeerEnabled(); + if (enable) { + String prefix = graphspace + "-"; + Map graphInfo = null; + try { + HugeClient client = this.authClient(graphspace, null); + + graphInfo = HubbleUtil.uncheckedCast( + client.vermeer().getGraphInfoByName(prefix + graph) + .get("graph")); + } catch (ServerException e) { + // if dashboard enables vermeer but server sets wrong vermeer + // address, return null + log.info("Failed to connect to the Vermeer service", + e.cause()); + return ImmutableMap.of(); + } + + if (graphInfo == null || graphInfo.isEmpty()) { + return ImmutableMap.of(); + } + + Map brief = new HashMap<>(); + + String name = getGraphFromVermeer(graphInfo.get("name").toString()); + brief.put("name", name); + brief.put("status", graphInfo.get("status").toString()); + String lastLoadTime = graphInfo.get("update_time").toString(); + // todo date format + brief.put("last_load_time", lastLoadTime); + return brief; + } + return ImmutableMap.of(); + } + + @GetMapping("list") + public Object listGraphs(@PathVariable("graphspace") String graphspace) { + // Get list of authorized graphs + HugeClient client = this.authClient(graphspace, null); + + boolean enable = isVermeerEnabled(); + Map vermeerInfo = getVermeerGraphs(graphspace, enable); + List> sortedGraphs = + graphsService.sortedGraphsProfile(client, graphspace, "", "", + enable, vermeerInfo); + + return ImmutableMap.of("graphs", sortedGraphs); + } + + @GetMapping + public Object queryPage(@PathVariable("graphspace") String graphspace, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "create_time", required = false, + defaultValue = "") String createTime, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphspace, null); + boolean enable = isVermeerEnabled(); + Map vermeerInfo = getVermeerGraphs(graphspace, enable); + Object result = this.graphsService.queryPage(client, graphspace, + getUser(), query, + createTime, pageNo, + pageSize, enable, + vermeerInfo); + + return result; + } + + @GetMapping("{graph}/get") + public Object get(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + Map vermeerInfo = getVermeerGraph(graphspace, graph); + Object result = graphsService.get(this.authClient(graphspace, graph), + graphspace, graph, vermeerInfo); + return result; + } + + @PostMapping("{graph}/statistics") + public void postStatistics( + @PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + HugeClient client = this.authClient(graphspace, graph); + graphsService.postStatistics(null, client, graphspace, graph); + } + + @GetMapping("{graph}/statistics") + public GraphStatisticsEntity getStatistics( + @PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + HugeClient client = this.authClient(graphspace, graph); + GraphStatisticsEntity result = + graphsService.getStatistics(client, graphspace, + graph); + return result; + } + + @PostMapping("{graph}") + public Object create(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestBody(required = false) + GraphCreateEntity request) { + String nickname = request == null ? graph : request.getNickname(); + String schema = request == null ? null : request.getSchema(); + if (StringUtils.isEmpty(nickname)) { + nickname = graph; + } + return this.graphsService.create(this.authClient(graphspace, null), + nickname, graph, schema); + } + + @PutMapping("{graph}") + public void update(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestBody GraphUpdateEntity request) { + + this.graphsService.update(this.authClient(graphspace, null), + request.getNickname(), graph); + } + + @DeleteMapping("{graph}") + public void delete(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestParam(value = "message", required = false, + defaultValue = "I'm sure to drop the graph") + String message) { + + Map vermeer = getVermeerGraph(graphspace, graph); + if (!vermeer.isEmpty()) { + HugeClient client = this.authClient(null, null); + try { + vermeerService.deleteGraph(client, graphspace, graph); + } catch (Exception e) { + // HugeGraph-Common assert request with delete method returns + // 202 or 204 + if (!e.getMessage().contains("deleted ok")) { + throw new ExternalException("delete vermeer graph error " + + "%s", e.getMessage()); + } + } + } + + this.graphsService.delete(this.authClient(graphspace, graph), graph, + message); + + // Clean Local DB Data + executeHistoryService.deleteByGraph(graphspace, graph); + gremlinCollectionService.deleteByGraph(graphspace, graph); + jobManagerService.removeByGraph(graphspace, graph); + } + + @PostMapping("{graph}/clear") + public void clearGraph(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + this.graphsService.clearGraph(this.authClient(graphspace, graph), graph); + } + + @PostMapping("{graph}/default") + public void setDefaultByCanonicalApi( + @PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + this.graphsService.setDefault(this.authClient(graphspace, null), graph); + } + + @DeleteMapping("{graph}/default") + public void unSetDefaultByCanonicalApi( + @PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + this.graphsService.unSetDefault(this.authClient(graphspace, graph), + graph); + } + + @GetMapping("default") + public Map getDefault( + @PathVariable("graphspace") String graphspace) { + return this.graphsService.getDefault(this.authClient(graphspace, null)); + } + + @PutMapping("{graph}/graph_read_mode") + public void graphReadMode(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestBody String mode) { + this.graphsService.graphReadMode(this.authClient(graphspace, graph), + graph, mode); + } + + @GetMapping("{graph}/graph_read_mode") + public Map graphReadMode( + @PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + Map status = new HashMap<>(); + GraphReadMode graphMode = this.graphsService.graphReadMode( + this.authClient(graphspace, graph), graph); + if ("all".equals(graphMode.string())) { + status.put("status", "0"); + } else if ("oltp_only".equals(graphMode.string())) { + status.put("status", "1"); + } + return status; + } + + @PostMapping("{graph}/clone") + public Object clone(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestBody GraphCloneEntity graphCloneEntity) { + return this.graphsService.clone(this.authClient(graphspace, graph), + graphCloneEntity.convertMap(graphspace, + graph)); + } + // + //@Data + //@NoArgsConstructor + //@AllArgsConstructor + //@Builder + //private static class GraphCloneEntity { + // @JsonProperty("target_graphspace") + // public String targetGraphSpace; + // + // @JsonProperty("target_graph") + // public String targetGraph; + // + // public Map convertMap() { + // return Map.of("target_graphspace", targetGraphSpace, + // "target_graph", targetGraph); + // } + //} +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java new file mode 100644 index 000000000..86809ccc7 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java @@ -0,0 +1,842 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.ingest; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.GraphConnection; +import org.apache.hugegraph.entity.enums.FileMappingStatus; +import org.apache.hugegraph.entity.enums.JobStatus; +import org.apache.hugegraph.entity.enums.LoadStatus; +import org.apache.hugegraph.entity.load.Datasource; +import org.apache.hugegraph.entity.load.EdgeMapping; +import org.apache.hugegraph.entity.load.FieldMappingItem; +import org.apache.hugegraph.entity.load.FileMapping; +import org.apache.hugegraph.entity.load.FileSetting; +import org.apache.hugegraph.entity.load.JobManager; +import org.apache.hugegraph.entity.load.LoadParameter; +import org.apache.hugegraph.entity.load.LoadTask; +import org.apache.hugegraph.entity.load.NullValues; +import org.apache.hugegraph.entity.load.ValueMappingItem; +import org.apache.hugegraph.entity.load.VertexMapping; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.load.DatasourceService; +import org.apache.hugegraph.service.load.FileMappingService; +import org.apache.hugegraph.service.load.JobManagerService; +import org.apache.hugegraph.service.load.LoadTaskService; +import org.apache.hugegraph.util.Ex; +import org.apache.hugegraph.util.FileUtil; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.UrlUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Log4j2 +@RestController +@RequestMapping(Constant.API_VERSION + "ingest") +public class IngestController extends BaseController { + + @Autowired + private JobManagerService jobManagerService; + @Autowired + private LoadTaskService loadTaskService; + @Autowired + private DatasourceService datasourceService; + @Autowired + private FileMappingService fileMappingService; + @Autowired + private HugeConfig config; + + // ===== Datasource endpoints ===== + + @GetMapping("/datasources/list") + public Response datasourceList( + @RequestParam(name = "query", required = false, defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, defaultValue = "10") int pageSize) { + IPage page = datasourceService.list(pageNo, pageSize, query); + return Response.builder().status(Constant.STATUS_OK).data(page).build(); + } + + @GetMapping("/datasources/{id}") + public Response datasourceGet(@PathVariable("id") int id) { + Datasource ds = datasourceService.get(id); + if (ds == null) { + return Response.builder().status(Constant.STATUS_NOT_FOUND) + .message("Datasource not found: " + id).build(); + } + return Response.builder().status(Constant.STATUS_OK).data(ds).build(); + } + + @PostMapping("/datasources") + public Response datasourceCreate(@RequestBody Datasource entity) { + datasourceService.save(entity); + return Response.builder().status(Constant.STATUS_OK) + .data(Map.of("datasource_id", entity.getId())).build(); + } + + @DeleteMapping("/datasources/{id}") + public Response datasourceDelete(@PathVariable("id") int id) { + datasourceService.remove(id); + return Response.builder().status(Constant.STATUS_OK).build(); + } + + @PostMapping("/datasources/delete") + public Response datasourceBatchDelete(@RequestBody List ids) { + datasourceService.removeBatch(ids); + return Response.builder().status(Constant.STATUS_OK).build(); + } + + @PostMapping("/files/upload") + public Response uploadFile(@RequestParam("file") MultipartFile file) { + Ex.check(file != null && !file.isEmpty(), "load.upload.file.cannot-be-empty"); + String fileName = FilenameUtils.getName(file.getOriginalFilename()); + Ex.check(StringUtils.isNotBlank(fileName) && + fileName.equals(file.getOriginalFilename()), + "load.upload.file.name.invalid"); + Long limit = this.config.get(HubbleOptions.UPLOAD_SINGLE_FILE_SIZE_LIMIT); + Ex.check(file.getSize() <= limit, "load.upload.file.exceed-single-size", + limit); + + Path uploadRoot = Paths.get(this.config.get(HubbleOptions.UPLOAD_FILE_LOCATION)) + .toAbsolutePath() + .normalize(); + Path target = uploadRoot.resolve("ingest") + .resolve(UUID.randomUUID().toString()) + .resolve(fileName) + .normalize(); + Ex.check(target.startsWith(uploadRoot), + "load.upload.file.path.outside-root", target, uploadRoot); + try { + FileUtils.forceMkdirParent(target.toFile()); + file.transferTo(target.toFile()); + } catch (IOException e) { + throw new InternalException("Failed to save ingest upload file %s", + e, fileName); + } + + Map data = new HashMap<>(); + data.put("file", target.toString()); + return Response.builder().status(Constant.STATUS_OK).data(data).build(); + } + + @GetMapping("/schemas") + public Response datasourceSchema(@RequestParam("datasource") int id) { + Datasource datasource = datasourceService.get(id); + if (datasource == null) { + return Response.builder().status(Constant.STATUS_NOT_FOUND) + .message("Datasource not found: " + id).build(); + } + + Map config = datasource.getDatasourceConfig(); + if (!"FILE".equals(config.get("type"))) { + return Response.builder().status(Constant.STATUS_BAD_REQUEST) + .message("Only FILE datasource schema is supported") + .build(); + } + + List header = this.stringList(config.get("header")); + if (!header.isEmpty()) { + return Response.builder().status(Constant.STATUS_OK) + .data(header).build(); + } + + FileSetting setting = this.buildFileSetting(config, Collections.emptyList(), + false); + ColumnInfo columns = this.readColumns(this.requireUploadFile(config), + setting, false); + return Response.builder().status(Constant.STATUS_OK) + .data(columns.names).build(); + } + + @PostMapping("/jdbc/check") + public Response checkJdbc() { + return Response.builder().status(Constant.STATUS_BAD_REQUEST) + .message("JDBC datasource check is not supported locally") + .build(); + } + + // ===== Task endpoints (job_manager table) ===== + + @PostMapping("/tasks") + public Response createTask(@RequestBody IngestTaskRequest request) { + Ex.check(request != null, "common.param.cannot-be-null-or-empty", + "task"); + Ex.check("ONCE".equals(request.taskScheduleType), + "Only ONCE ingest tasks are currently supported"); + Datasource datasource = datasourceService.get(request.datasourceId); + Ex.check(datasource != null, "Datasource not found: %s", + request.datasourceId); + Map dsConfig = datasource.getDatasourceConfig(); + Ex.check("FILE".equals(dsConfig.get("type")), + "Only FILE datasource ingest tasks are currently supported"); + + IngestStruct struct = request.firstStruct(); + Map input = new HashMap<>(dsConfig); + if (struct.input != null) { + input.putAll(struct.input); + } + + String graphSpace = request.ingestionOption.graphspace; + String graph = request.ingestionOption.graph; + File sourceFile = this.requireUploadFile(input); + long totalSize = sourceFile.length(); + List header = this.stringList(input.get("header")); + boolean hasPhysicalHeader = !this.stringList(dsConfig.get("header")) + .isEmpty(); + FileSetting setting = this.buildFileSetting(input, header, + hasPhysicalHeader); + if (setting.getColumnNames() == null || + setting.getColumnNames().isEmpty()) { + ColumnInfo columns = this.readColumns(sourceFile, setting, + hasPhysicalHeader); + setting.setColumnNames(columns.names); + setting.setColumnValues(columns.values); + } + + JobManager job = JobManager.builder() + .graphSpace(graphSpace) + .graph(graph) + .jobName(request.taskName) + .jobSize(totalSize) + .jobStatus(JobStatus.SETTING) + .createTime(HubbleUtil.nowDate()) + .updateTime(HubbleUtil.nowDate()) + .build(); + FileMapping mapping = new FileMapping(graphSpace, graph, + sourceFile.getName(), + sourceFile.getPath()); + mapping.setFileStatus(FileMappingStatus.COMPLETED); + mapping.setTotalSize(totalSize); + mapping.setTotalLines(FileUtil.countLines(sourceFile.getPath())); + mapping.setFileSetting(setting); + mapping.setLoadParameter(new LoadParameter()); + mapping.setVertexMappings(this.vertexMappings(struct.vertices)); + mapping.setEdgeMappings(this.edgeMappings(struct.edges)); + + boolean started = false; + try { + this.jobManagerService.save(job); + mapping.setJobId(job.getId()); + this.fileMappingService.save(mapping); + + GraphConnection connection = this.graphConnection(graphSpace, graph); + HugeClient client = this.authClient(graphSpace, graph); + LoadTask task = this.loadTaskService.start(connection, mapping, + client); + started = true; + Map data = new HashMap<>(); + data.put("task_id", job.getId()); + data.put("job_id", task.getId()); + return Response.builder().status(Constant.STATUS_OK) + .data(data).build(); + } finally { + if (job.getId() != null) { + job.setJobStatus(started ? JobStatus.LOADING : + JobStatus.FAILED); + job.setUpdateTime(HubbleUtil.nowDate()); + this.jobManagerService.update(job); + } + } + } + + @GetMapping("/tasks/list") + public Response taskList( + @RequestParam(name = "query", required = false, defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, defaultValue = "10") int pageSize) { + + // list all jobs across all graphspaces - use empty strings to get all + // We need to query without graphspace/graph filter for the ingest view + IPage page = jobManagerService.listAll(pageNo, pageSize, query); + + IPage result = page.convert(job -> { + TaskVO vo = new TaskVO(); + vo.taskId = job.getId(); + vo.taskName = job.getJobName(); + vo.taskScheduleType = "ONCE"; + vo.taskScheduleStatus = toScheduleStatus(job.getJobStatus()); + vo.createTime = job.getCreateTime(); + vo.creator = ""; + + // Build ingestion_option from job fields + Map option = new HashMap<>(); + option.put("graphspace", job.getGraphSpace()); + option.put("graph", job.getGraph()); + vo.ingestionOption = option; + + // Build ingestion_mapping with structs from file mappings + List tasks = loadTaskService.taskListByJob(job.getId()); + List> structs = tasks.stream().map(t -> { + Map struct = new HashMap<>(); + Map input = new HashMap<>(); + input.put("type", "FILE"); + input.put("path", t.getFileName()); + struct.put("input", input); + return struct; + }).collect(Collectors.toList()); + Map mapping = new HashMap<>(); + mapping.put("structs", structs); + vo.ingestionMapping = mapping; + + // Build last_metrics from latest load task + if (!tasks.isEmpty()) { + LoadTask latest = tasks.get(tasks.size() - 1); + Map metrics = new HashMap<>(); + metrics.put("status", latest.getStatus().name()); + metrics.put("load_progress", latest.getLoadProgress()); + vo.lastMetrics = metrics; + } + + // Build job_summary + JobSummaryVO summary = new JobSummaryVO(); + for (LoadTask t : tasks) { + if (t.getStatus() == LoadStatus.SUCCEED) { + summary.successCount++; + } else if (t.getStatus() == LoadStatus.FAILED) { + summary.failedCount++; + } else if (t.getStatus().inRunning()) { + summary.runningCount++; + } + } + vo.jobSummary = summary; + + return vo; + }); + + return Response.builder().status(Constant.STATUS_OK).data(result).build(); + } + + @GetMapping("/tasks/{id}") + public Response taskDetail(@PathVariable("id") int id) { + JobManager job = jobManagerService.get(id); + if (job == null) { + return Response.builder().status(Constant.STATUS_NOT_FOUND) + .message("Task not found: " + id).build(); + } + return Response.builder().status(Constant.STATUS_OK).data(job).build(); + } + + @DeleteMapping("/tasks/{id}") + public Response deleteTask(@PathVariable("id") int id) { + jobManagerService.remove(id); + return Response.builder().status(Constant.STATUS_OK).build(); + } + + @PutMapping("/tasks/{id}/enable") + public Response enableTask(@PathVariable("id") int id) { + JobManager job = jobManagerService.get(id); + if (job == null) { + return Response.builder().status(Constant.STATUS_NOT_FOUND) + .message("Task not found: " + id).build(); + } + job.setJobStatus(JobStatus.DEFAULT); + jobManagerService.update(job); + return Response.builder().status(Constant.STATUS_OK).build(); + } + + @PutMapping("/tasks/{id}/disable") + public Response disableTask(@PathVariable("id") int id) { + JobManager job = jobManagerService.get(id); + if (job == null) { + return Response.builder().status(Constant.STATUS_NOT_FOUND) + .message("Task not found: " + id).build(); + } + job.setJobStatus(JobStatus.FAILED); + jobManagerService.update(job); + return Response.builder().status(Constant.STATUS_OK).build(); + } + + // ===== Job endpoints (load_task table) ===== + + @GetMapping("/jobs/list") + public Response jobList( + @RequestParam(name = "taskid", required = false, defaultValue = "0") int taskId, + @RequestParam(name = "page_no", required = false, defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, defaultValue = "10") int pageSize) { + + List tasks = loadTaskService.taskListByJob(taskId); + + // Manual pagination + int total = tasks.size(); + int fromIndex = Math.min((pageNo - 1) * pageSize, total); + int toIndex = Math.min(fromIndex + pageSize, total); + List pageData = tasks.subList(fromIndex, toIndex); + + List records = pageData.stream().map(t -> { + JobVO vo = new JobVO(); + vo.jobId = t.getId(); + vo.taskId = t.getJobId(); + vo.jobStatus = t.getStatus().name(); + vo.jobMessage = ""; + vo.createTime = t.getCreateTime(); + + JobMetricsVO metrics = new JobMetricsVO(); + metrics.totalCount = t.getFileReadLines() != null ? t.getFileReadLines() : 0L; + long durationMs = t.getDuration() != null ? t.getDuration() : 0L; + long durationSec = durationMs > 0 ? + Math.max(1L, durationMs / 1000L) : 1L; + metrics.avgRate = durationMs > 0 ? (float) metrics.totalCount / durationSec : 0f; + metrics.curRate = t.getStatus().inRunning() ? metrics.avgRate : 0f; + metrics.totalTime = durationMs; + vo.jobMetrics = metrics; + + return vo; + }).collect(Collectors.toList()); + + Map result = new HashMap<>(); + result.put("records", records); + result.put("total", total); + result.put("size", pageSize); + result.put("current", pageNo); + + return Response.builder().status(Constant.STATUS_OK).data(result).build(); + } + + @GetMapping("/jobs/{id}") + public Response jobDetail(@PathVariable("id") int id) { + LoadTask task = loadTaskService.get(id); + if (task == null) { + return Response.builder().status(Constant.STATUS_NOT_FOUND) + .message("Job not found: " + id).build(); + } + return Response.builder().status(Constant.STATUS_OK).data(task).build(); + } + + @DeleteMapping("/jobs/{id}") + public Response deleteJob(@PathVariable("id") int id) { + loadTaskService.remove(id); + return Response.builder().status(Constant.STATUS_OK).build(); + } + + // ===== Metrics ===== + + @GetMapping("/metrics/task") + public Response metricsTask() { + List all = jobManagerService.listAll(); + all.forEach(jobManagerService::refreshStatus); + + long runningOnce = 0; + long runningCron = 0; + long runningRealtime = 0; + long todoOnce = 0; + long todoCron = 0; + long todoRealtime = 0; + + for (JobManager job : all) { + if (job.getJobStatus() == JobStatus.LOADING) { + runningOnce++; + } else if (job.getJobStatus() == JobStatus.DEFAULT || + job.getJobStatus() == JobStatus.SETTING) { + todoOnce++; + } + } + + Map todo = new HashMap<>(); + todo.put("ONCE", todoOnce); + todo.put("CRON", todoCron); + todo.put("REALTIME", todoRealtime); + + Map running = new HashMap<>(); + running.put("ONCE", runningOnce); + running.put("CRON", runningCron); + running.put("REALTIME", runningRealtime); + + Map data = new HashMap<>(); + data.put("total_realtime_size", 0); + data.put("total_other_size", all.size()); + data.put("todo", todo); + data.put("running", running); + + return Response.builder().status(Constant.STATUS_OK).data(data).build(); + } + + // ===== Helpers ===== + + private GraphConnection graphConnection(String graphSpace, String graph) { + GraphConnection connection = new GraphConnection(); + connection.setCluster(config.get(HubbleOptions.PD_CLUSTER)); + connection.setRouteType(config.get(HubbleOptions.ROUTE_TYPE)); + connection.setPdPeers(config.get(HubbleOptions.PD_PEERS)); + connection.setGraphSpace(graphSpace); + connection.setGraph(graph); + connection.setToken(this.getToken()); + connection.setUsername(this.getUser()); + connection.setPassword(this.getCredentialPassword()); + if (!config.get(HubbleOptions.PD_ENABLED)) { + UrlUtil.Host host = UrlUtil.parseHost(config.get( + HubbleOptions.SERVER_URL)); + connection.setProtocol(host.getScheme()); + connection.setHost(host.getHost()); + connection.setPort(host.getPort()); + } + return connection; + } + + private File requireUploadFile(Map config) { + String path = this.string(config.get("path")); + Ex.check(StringUtils.isNotBlank(path), + "Datasource file path is empty"); + return this.fileMappingService.requirePathUnderUploadRoot(path); + } + + private FileSetting buildFileSetting(Map input, + List header, + boolean hasPhysicalHeader) { + FileSetting setting = new FileSetting(); + setting.setHasHeader(hasPhysicalHeader); + if (!header.isEmpty()) { + setting.setColumnNames(header); + } + setting.setFormat(this.stringOrDefault(input.get("format"), "CSV")); + setting.setDelimiter(this.stringOrDefault(input.get("delimiter"), ",")); + setting.setCharset(this.stringOrDefault(input.get("charset"), "UTF-8")); + setting.setDateFormat(this.stringOrDefault(input.get("date_format"), + "yyyy-MM-dd HH:mm:ss")); + setting.setTimeZone(this.stringOrDefault(input.get("time_zone"), + "GMT+8")); + Object skippedLine = input.get("skipped_line"); + if (skippedLine instanceof Map) { + setting.setSkippedLine(this.stringOrDefault( + ((Map) skippedLine).get("regex"), "(^#|^//).*")); + } else { + setting.setSkippedLine(this.stringOrDefault(skippedLine, + "(^#|^//).*")); + } + setting.changeFormatIfNeeded(); + return setting; + } + + private ColumnInfo readColumns(File file, FileSetting setting, + boolean hasPhysicalHeader) { + try (BufferedReader reader = Files.newBufferedReader(file.toPath())) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.matches(setting.getSkippedLine())) { + break; + } + } + Ex.check(line != null, "The file has no data line can treat as header"); + + List firstLine = this.splitLine(line, setting.getDelimiter()); + if (hasPhysicalHeader) { + String sample = reader.readLine(); + List values = sample == null ? Collections.emptyList() : + this.splitLine(sample, + setting.getDelimiter()); + return new ColumnInfo(firstLine, values); + } + + List names = new ArrayList<>(); + for (int i = 1; i <= firstLine.size(); i++) { + names.add("col-" + i); + } + return new ColumnInfo(names, firstLine); + } catch (IOException e) { + throw new InternalException("Failed to read columns from file %s", + e, file); + } + } + + private List splitLine(String line, String delimiter) { + if (line == null) { + return Collections.emptyList(); + } + String[] values = StringUtils.split(line, delimiter); + List list = new ArrayList<>(); + if (values != null) { + Collections.addAll(list, values); + } + return list; + } + + private Set vertexMappings(List> raw) { + Set mappings = new LinkedHashSet<>(); + if (raw == null) { + return mappings; + } + for (int i = 0; i < raw.size(); i++) { + Map item = raw.get(i); + List idFields = this.stringList(item.get("id_fields")); + String id = this.string(item.get("id")); + if (idFields.isEmpty() && StringUtils.isNotBlank(id)) { + idFields = Collections.singletonList(id); + } + VertexMapping mapping = VertexMapping.builder() + .idFields(idFields) + .build(); + this.fillElementMapping(mapping, item, "vertex-" + i); + mappings.add(mapping); + } + return mappings; + } + + private Set edgeMappings(List> raw) { + Set mappings = new LinkedHashSet<>(); + if (raw == null) { + return mappings; + } + for (int i = 0; i < raw.size(); i++) { + Map item = raw.get(i); + EdgeMapping mapping = EdgeMapping.builder() + .sourceFields(this.stringList( + item.get("source"))) + .targetFields(this.stringList( + item.get("target"))) + .build(); + this.fillElementMapping(mapping, item, "edge-" + i); + mappings.add(mapping); + } + return mappings; + } + + private void fillElementMapping( + org.apache.hugegraph.entity.load.ElementMapping mapping, + Map item, String id) { + mapping.setId(id); + mapping.setLabel(this.string(item.get("label"))); + mapping.setFieldMappings(this.fieldMappings( + item.get("field_mapping"))); + mapping.setValueMappings(this.valueMappings( + item.get("value_mapping"))); + mapping.setNullValues(this.nullValues(item.get("null_values"))); + } + + private List fieldMappings(Object raw) { + if (!(raw instanceof Map)) { + return Collections.emptyList(); + } + List mappings = new ArrayList<>(); + for (Map.Entry entry : ((Map) raw).entrySet()) { + mappings.add(FieldMappingItem.builder() + .columnName(this.string(entry.getKey())) + .mappedName(this.string(entry.getValue())) + .build()); + } + return mappings; + } + + private List valueMappings(Object raw) { + if (!(raw instanceof Map)) { + return Collections.emptyList(); + } + List mappings = new ArrayList<>(); + for (Map.Entry entry : ((Map) raw).entrySet()) { + List values = new ArrayList<>(); + if (entry.getValue() instanceof Map) { + for (Map.Entry valueEntry : + ((Map) entry.getValue()).entrySet()) { + values.add(ValueMappingItem.ValueItem.builder() + .columnValue(this.string(valueEntry.getKey())) + .mappedValue(this.string(valueEntry.getValue())) + .build()); + } + } + mappings.add(ValueMappingItem.builder() + .columnName(this.string(entry.getKey())) + .values(values) + .build()); + } + return mappings; + } + + private NullValues nullValues(Object raw) { + Set checked = new LinkedHashSet<>(); + Set customized = new LinkedHashSet<>(); + if (raw instanceof Map) { + checked.addAll(this.objectSet(((Map) raw).get("checked"))); + customized.addAll(this.objectSet(((Map) raw).get( + "customized"))); + } else { + checked.addAll(this.objectSet(raw)); + } + return NullValues.builder() + .checked(checked) + .customized(customized) + .build(); + } + + private Set objectSet(Object raw) { + Set values = new LinkedHashSet<>(); + if (raw instanceof Iterable) { + for (Object item : (Iterable) raw) { + values.add(item); + } + } else if (raw != null) { + values.add(raw); + } + return values; + } + + private List stringList(Object raw) { + if (raw instanceof Iterable) { + List values = new ArrayList<>(); + for (Object item : (Iterable) raw) { + values.add(this.string(item)); + } + return values; + } + if (raw == null || StringUtils.isBlank(raw.toString())) { + return Collections.emptyList(); + } + return Collections.singletonList(raw.toString()); + } + + private String stringOrDefault(Object raw, String defaultValue) { + String value = this.string(raw); + return StringUtils.isBlank(value) ? defaultValue : value; + } + + private String string(Object raw) { + return raw == null ? null : raw.toString(); + } + + private String toScheduleStatus(JobStatus status) { + if (status == JobStatus.FAILED) { + return "DISABLE"; + } + return "ENABLE"; + } + + // ===== VOs ===== + + @Data + static class TaskVO { + @JsonProperty("task_id") Integer taskId; + @JsonProperty("task_name") String taskName; + @JsonProperty("task_schedule_type") String taskScheduleType; + @JsonProperty("task_schedule_status") String taskScheduleStatus; + @JsonProperty("ingestion_option") Object ingestionOption; + @JsonProperty("ingestion_mapping") Object ingestionMapping; + @JsonProperty("last_metrics") Object lastMetrics; + @JsonProperty("job_summary") JobSummaryVO jobSummary; + @JsonProperty("create_time") Date createTime; + @JsonProperty("creator") String creator; + } + + @Data + static class JobSummaryVO { + @JsonProperty("success_count") int successCount; + @JsonProperty("failed_count") int failedCount; + @JsonProperty("running_count") int runningCount; + } + + @Data + static class JobVO { + @JsonProperty("job_id") Integer jobId; + @JsonProperty("task_id") Integer taskId; + @JsonProperty("job_status") String jobStatus; + @JsonProperty("job_message") String jobMessage; + @JsonProperty("job_metrics") JobMetricsVO jobMetrics; + @JsonProperty("create_time") Date createTime; + } + + @Data + static class JobMetricsVO { + @JsonProperty("total_count") long totalCount; + @JsonProperty("avg_rate") float avgRate; + @JsonProperty("cur_rate") float curRate; + @JsonProperty("total_time") long totalTime; + } + + @Data + static class IngestTaskRequest { + @JsonProperty("task_name") String taskName; + @JsonProperty("datasource_id") Integer datasourceId; + @JsonProperty("task_schedule_type") String taskScheduleType; + @JsonProperty("ingestion_option") IngestionOption ingestionOption; + @JsonProperty("ingestion_mapping") IngestionMapping ingestionMapping; + + IngestStruct firstStruct() { + Ex.check(this.ingestionOption != null, + "common.param.cannot-be-null-or-empty", + "ingestion_option"); + Ex.check(this.ingestionMapping != null && + this.ingestionMapping.structs != null && + !this.ingestionMapping.structs.isEmpty(), + "common.param.cannot-be-null-or-empty", + "ingestion_mapping.structs"); + return this.ingestionMapping.structs.get(0); + } + } + + @Data + static class IngestionOption { + @JsonProperty("graphspace") String graphspace; + @JsonProperty("graph") String graph; + } + + @Data + static class IngestionMapping { + @JsonProperty("structs") List structs; + } + + @Data + static class IngestStruct { + @JsonProperty("input") Map input; + @JsonProperty("vertices") List> vertices; + @JsonProperty("edges") List> edges; + } + + static class ColumnInfo { + private final List names; + private final List values; + + ColumnInfo(List names, List values) { + this.names = names; + this.values = values; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/langchain/LangChainController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/langchain/LangChainController.java new file mode 100644 index 000000000..8c1695fe0 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/langchain/LangChainController.java @@ -0,0 +1,785 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.langchain; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.HashMap; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.controller.query.GremlinController; +import org.apache.hugegraph.driver.SchemaManager; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.entity.query.JsonView; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.structure.schema.EdgeLabel; +import org.apache.hugegraph.structure.schema.VertexLabel; +import org.apache.hugegraph.config.ConfigOption; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.util.Ex; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.compress.utils.Lists; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.util.E; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; + +/** + * langchain controller + */ +@Log4j2 +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs/{graph}") +public class LangChainController extends BaseController { + + private static final String DEFAULT_PYTHON_FILE = "langchaincode/excute_langchain.py"; + private static final String DEFAULT_PYTHON_SCRIPT = "excute_langchain.py"; + + private static final String G_V = "g.v"; + private static final String G_E = "g.e"; + + private static final String WENXIN_4_MODEL = "wenxin4"; + private static final String GPT_4_MODEL = "gpt4"; + + private static final List DEFAULT_MODEL = Arrays.asList(WENXIN_4_MODEL, GPT_4_MODEL); + + @Autowired + private QueryService queryService; + + @Autowired + private HugeConfig config; + + @PostMapping("langchain") + public Object langchain(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody RequestLangChainParams requestLangChainParams) { + E.checkNotNull(requestLangChainParams, "params must not be null"); + log.info("LangChainController langchain params:{}"); + this.checkParams(requestLangChainParams); + this.checkModelParams(requestLangChainParams); + this.checkUserParam(requestLangChainParams); + + this.tryLogin(graphSpace, graph, + requestLangChainParams.userName, requestLangChainParams.password); + + return this.langChainQuery(graphSpace, graph, requestLangChainParams); + } + + @PostMapping("langchain/hubble") + public Object langchainHubble(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody RequestLangChainParams requestLangChainParams) { + E.checkNotNull(requestLangChainParams, "params must not be null"); + log.info("LangChainController langchain request model:{} file:{}", + requestLangChainParams.model, requestLangChainParams.fileName); + this.checkParams(requestLangChainParams); + + return this.langChainQuery(graphSpace, graph, requestLangChainParams); + } + + private ResponseLangChain langChainQuery(String graphSpace, String graph, + RequestLangChainParams requestLangChainParams) { + HugeClient client = this.authClient(graphSpace, graph); + SchemaManager schemaManager = client.schema(); + List vertexLabels = schemaManager.getVertexLabels(); + List edgeLabels = schemaManager.getEdgeLabels(); + String schema = JsonUtil.toJson(this.getBigModelSchema(vertexLabels, edgeLabels)); + log.info("langchain schema:{}", schema); + + String filePath = this.resolvePythonScriptPath( + requestLangChainParams.fileName); + log.info("LangChainController filePath:{}", filePath); + + List result = + this.excutePythonRuntime(requestLangChainParams.pythonPath, + filePath, requestLangChainParams.query, + requestLangChainParams.openKey, schema, + requestLangChainParams.model, + requestLangChainParams.ernieClientId, + requestLangChainParams.ernieClientSecret); + if (CollectionUtils.isEmpty(result)) { + return this.generateResponseLangChain(requestLangChainParams.query, + "LangChain not generate gremlin"); + } else { + return this.generateResponseLangChain(requestLangChainParams.query, + result.get(result.size() - 1)); + } + } + + @PostMapping("langchain/schema") + public Object langchainSchema(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody RequestLangChainParams requestLangChainParams) { + E.checkNotNull(requestLangChainParams, "params must not be null"); + log.info("LangChainController langchain schema request username:{}", + requestLangChainParams.userName); + this.checkUserParam(requestLangChainParams); + + this.tryLogin(graphSpace, graph, + requestLangChainParams.userName, requestLangChainParams.password); + + HugeClient client = this.authClient(graphSpace, graph); + SchemaManager schemaManager = client.schema(); + List vertexLabels = schemaManager.getVertexLabels(); + List edgeLabels = schemaManager.getEdgeLabels(); + HashMap schema = this.getBigModelSchema(vertexLabels, edgeLabels); + return schema; + } + + @PostMapping("gremlin") + public Object gremlin(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody RequestLangChainParams requestLangChainParams) { + E.checkNotNull(requestLangChainParams, "params must not be null"); + GremlinQuery query = new GremlinQuery(); + query.setContent(requestLangChainParams.query); + this.checkParamsValid(query); + this.checkUserParam(requestLangChainParams); + + this.tryLogin(graphSpace, graph, + requestLangChainParams.userName, requestLangChainParams.password); + + try { + HugeClient client = this.authClient(graphSpace, graph); + JsonView result = + this.queryService.executeSingleGremlinQuery(client, query); + return result.getData(); + } catch (Throwable e) { + throw e; + } + } + + @PostMapping("langchain_no_schema") + public Object langchainNoSchema(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody RequestLangChainParams requestLangChainParams) { + E.checkNotNull(requestLangChainParams, "params must not be null"); + log.info("LangChainController langchain no schema request model:{} file:{}", + requestLangChainParams.model, requestLangChainParams.fileName); + + this.checkParams(requestLangChainParams); + this.checkModelParams(requestLangChainParams); + + String filePath = this.resolvePythonScriptPath( + requestLangChainParams.fileName); + log.info("LangChainController filePath:{}", filePath); + + List result = + this.excutePythonByProcessBuilder( + requestLangChainParams.pythonPath, filePath, + requestLangChainParams.query, + requestLangChainParams.openKey, + requestLangChainParams.graphSchema, + requestLangChainParams.model, + requestLangChainParams.ernieClientId, + requestLangChainParams.ernieClientSecret); + if (CollectionUtils.isEmpty(result)) { + return this.generateResponseLangChain(requestLangChainParams.query, + "LangChain not generate gremlin"); + } else { + return this.generateResponseLangChain(requestLangChainParams.query, + result.get(result.size() - 1)); + } + } + + private void tryLogin(String graphSpace, String graph, + String username, String password) { + log.info("Attempting to login username:{}", username); + + E.checkNotNull(username, "username cannot be null"); + E.checkNotNull(password, "password cannot be null"); + String token = this.getToken(); + if (StringUtils.isNotEmpty(token)) { + log.info("Attempting to login token exist, username:{}", username); + return; + } + if (Objects.isNull(this.getToken())) { + log.error("Attempting to login failed, username:{}", username); + throw new IllegalStateException("login failed"); + } + } + + /** + * + * @param pythonPath + * @param pythonScriptPath + * @param query + * @param openKey + * @param graphSchema + * @return + */ + private List excutePythonRuntime(String pythonPath, + String pythonScriptPath, + String query, + String openKey, + String graphSchema, + String model, + String ernieClientId, + String ernieClientSecret) { + String[] args1 = this.getExcuteArgs(pythonPath, pythonScriptPath, + query, openKey, graphSchema, + model, ernieClientId, + ernieClientSecret); + return this.executePythonProcess(args1, model, + this.secretValues(openKey, + ernieClientSecret)); + } + + /** + * 使用ProcessBuilder执行python脚本 + * @param pythonPath + * @param pythonScriptPath + * @param query + * @param openKey + * @param graphSchema + * @return + */ + private List excutePythonByProcessBuilder(String pythonPath, + String pythonScriptPath, + String query, + String openKey, + String graphSchema, + String model, + String ernieClientId, + String ernieClientSecret) { + String[] args1 = this.getExcuteArgs(pythonPath, pythonScriptPath, + query, openKey, graphSchema, + model, ernieClientId, + ernieClientSecret); + return this.executePythonProcess(args1, model, + this.secretValues(openKey, + ernieClientSecret)); + } + + private String resolvePythonScriptPath(String fileName) { + E.checkArgument(StringUtils.isNotBlank(fileName), + "fileName must not be blank"); + Path requested = Paths.get(fileName).normalize(); + E.checkArgument(!requested.isAbsolute() && + requested.getFileName() != null && + (requested.getNameCount() == 1 || + DEFAULT_PYTHON_FILE.equals(requested.toString())), + "python file is not allowed"); + + String scriptName = requested.getFileName().toString(); + List allowlist = + this.configValue(HubbleOptions.LANGCHAIN_SCRIPT_ALLOWLIST); + + String safeScriptName = null; + for (String allowed : allowlist) { + if (allowed.equals(scriptName)) { + safeScriptName = allowed; + break; + } + } + if (safeScriptName == null) { + throw new IllegalArgumentException("python file is not allowed"); + } + + Path root = Paths.get(this.configValue(HubbleOptions.LANGCHAIN_SCRIPT_DIR)) + .normalize(); + if (!root.toFile().isDirectory()) { + throw new IllegalArgumentException("langchain script dir not exist"); + } + Path script = root.resolve(safeScriptName).normalize(); + if (!script.startsWith(root)) { + throw new IllegalArgumentException("python file is not allowed"); + } + if (!script.toFile().exists()) { + throw new IllegalArgumentException("python file not exist"); + } + return script.toString(); + } + + private List executePythonProcess(String[] args, String model, + Set secrets) { + log.info("lang chain execute python command: {}", + this.sanitizeCommandForLog(args)); + + ProcessBuilder pb = new ProcessBuilder(args); + pb.redirectErrorStream(false); + + List lineList = new ArrayList<>(); + List errorList = new ArrayList<>(); + Process process; + try { + process = pb.start(); + Thread stdout = this.readProcessStream(process.getInputStream(), + lineList, "stdout", secrets); + Thread stderr = this.readProcessStream(process.getErrorStream(), + errorList, "stderr", secrets); + int timeout = this.configValue(HubbleOptions.LANGCHAIN_EXECUTE_TIMEOUT); + if (!process.waitFor(timeout, TimeUnit.SECONDS)) { + process.destroyForcibly(); + throw new IllegalStateException("LangChain python execution timeout"); + } + stdout.join(TimeUnit.SECONDS.toMillis(1L)); + stderr.join(TimeUnit.SECONDS.toMillis(1L)); + + int exitCode = process.exitValue(); + if (exitCode != 0) { + log.error("LangChain python stderr:{}", + this.redactLines(errorList, secrets)); + throw new IllegalStateException( + "LangChain python exited with code " + exitCode); + } + + if (!this.judgeResultSuccess(lineList, model)) { + List redactedLines = this.redactLines(lineList, secrets); + this.calculateError(redactedLines, model); + log.error("excutePython lineList:{}", redactedLines); + lineList.clear(); + } else { + lineList = getGremlinResults(lineList, model); + } + } catch (IOException e) { + throw new IllegalStateException("LangChain python execution failed", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("LangChain python execution interrupted", e); + } + return lineList; + } + + private Thread readProcessStream(InputStream stream, List lines, + String streamName, Set secrets) { + Thread thread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + log.info("execute {} ret:{}", streamName, + this.redact(line, secrets)); + } + } catch (IOException e) { + log.error("Read LangChain process {} failed", streamName, e); + } + }, "langchain-" + streamName + "-reader"); + thread.setDaemon(true); + thread.start(); + return thread; + } + + private Set secretValues(String openKey, String ernieClientSecret) { + Set secrets = new java.util.HashSet<>(); + if (StringUtils.isNotEmpty(openKey)) { + secrets.add(openKey); + } + if (StringUtils.isNotEmpty(ernieClientSecret)) { + secrets.add(ernieClientSecret); + } + return secrets; + } + + private List redactLines(List lines, Set secrets) { + List redacted = new ArrayList<>(lines.size()); + for (String line : lines) { + redacted.add(this.redact(line, secrets)); + } + return redacted; + } + + private String redact(String value, Set secrets) { + String redacted = value; + for (String secret : secrets) { + redacted = StringUtils.replace(redacted, secret, "******"); + } + return redacted; + } + + private String sanitizeCommandForLog(String[] args) { + List sanitized = new ArrayList<>(); + for (int i = 0; i < args.length; i++) { + sanitized.add(args[i]); + if ("--open_key".equals(args[i]) || + "--ernie_client_secret".equals(args[i])) { + if (i + 1 < args.length) { + sanitized.add("******"); + i++; + } + } + } + return String.join(" ", sanitized); + } + + private List getGremlinResults(List lineList, String model) { + if (GPT_4_MODEL.equals(model)) { + return Arrays.asList(this.getGpt4GremlinResults(lineList)); + } else if (WENXIN_4_MODEL.equals(model)) { + return Arrays.asList(this.getWenXin4GremlinResults(lineList)); + } + return Collections.emptyList(); + } + + private String getGpt4GremlinResults(List lineList) { + for (String line : lineList) { + String tmp = line.toLowerCase(); + if (tmp.contains(G_V) || tmp.contains(G_E)) { + if (!line.startsWith(G_V) || !line.startsWith(G_E)) { + line = cutGremlin(line); + } + return line; + } + } + return StringUtils.EMPTY; + } + + private String getWenXin4GremlinResults(List lineList) { + for (String line : lineList) { + String tmp = line.toLowerCase(); + if (tmp.contains(G_V) || tmp.contains(G_E)) { + if (!line.startsWith(G_V) || !line.startsWith(G_E)) { + line = cutGremlin(line); + } + return line; + } + } + return StringUtils.EMPTY; + } + + private String cutGremlin(String line) { + int start = 0; + int end = 0; + for (int i = 0; i < line.length(); i++) { + if (line.charAt(i) == 'g') { + start = i; + break; + } + } + for (int i = line.length() - 1; i >= 0; i--) { + if (line.charAt(i) == ')') { + end = i + 1; + break; + } + } + return line.substring(start, end); + } + + /** + * 判断结果是否正确 + * @param lineList + * @return + */ + private boolean judgeResultSuccess(List lineList, String model) { + if (GPT_4_MODEL.equals(model)) { + return this.judgeGpt4ResultSuccess(lineList); + } else if (WENXIN_4_MODEL.equals(model)) { + return this.judgeWenXin4ResultSuccess(lineList); + } + return false; + } + + private boolean judgeGpt4ResultSuccess(List lineList) { + if (CollectionUtils.isEmpty(lineList)) { + return false; + } + for (String line : lineList) { + if (line.contains("Finished chain") || + line.toLowerCase().contains(G_E) || + line.toLowerCase().contains(G_V)) { + return true; + } + } + return false; + } + + private boolean judgeWenXin4ResultSuccess(List lineList) { + if (CollectionUtils.isEmpty(lineList)) { + return false; + } + for (String line : lineList) { + if (line.contains("```") || line.toLowerCase().contains(G_E) || + line.toLowerCase().contains(G_V)) { + return true; + } + } + return true; + } + + /** + * 输出error 信息 + * @param proc + * @return + */ + private void calculateError(List proc, String model) { + if (GPT_4_MODEL.equals(model)) { + this.calculateGpt4Error(proc); + } else if (WENXIN_4_MODEL.equals(model)) { + this.calculateWenXin4Error(proc); + } + } + + private void calculateGpt4Error(List proc) { + try { + StringBuilder sb = new StringBuilder(); + for (String line : proc) { + sb.append(line).append("\n"); + } + log.error("calculateError error {}", sb.toString()); + } catch (Exception e) { + log.error("calculateError error", e); + } + } + + private void calculateWenXin4Error(List proc) { + try { + StringBuilder sb = new StringBuilder(); + for (String line : proc) { + sb.append(line).append("\n"); + } + log.error("calculateError error {}", sb.toString()); + } catch (Exception e) { + log.error("calculateError error", e); + } + } + + /** + * 生成返回结果 + * @param query + * @param gremlin + * @return + */ + private ResponseLangChain generateResponseLangChain(String query, String gremlin) { + return ResponseLangChain.builder().gremlin(gremlin).query(query).build(); + } + + private void checkUserParam(RequestLangChainParams requestLangChainParams) { + E.checkNotNull(requestLangChainParams.getUserName(), + "params username must not be null"); + E.checkNotNull(requestLangChainParams.getPassword(), + "params password must not be null"); + } + + private void checkParams(RequestLangChainParams requestLangChainParams) { + E.checkNotNull(requestLangChainParams, "params must not be null"); + E.checkNotNull(requestLangChainParams.getQuery(), "params query must not be null"); + E.checkNotNull(requestLangChainParams.getModel(), "params model must not be null"); + E.checkState(DEFAULT_MODEL.contains(requestLangChainParams.getModel()), + "mode must be [\"wenxin4\", \"gpt4\"]"); + } + + private void checkModelParams(RequestLangChainParams requestLangChainParams) { + if (GPT_4_MODEL.equals(requestLangChainParams.getModel())) { + E.checkNotNull(requestLangChainParams.getOpenKey(), + "params open_key must not be null"); + } + if (WENXIN_4_MODEL.equals(requestLangChainParams.getModel())) { + E.checkNotNull(requestLangChainParams.getErnieClientId(), + "params ernie_client_id must not be null"); + E.checkNotNull(requestLangChainParams.getErnieClientSecret(), + "params ernie_client_secret must not be null"); + } + } + + private String[] getExcuteArgs(String pythonPath, + String pythonScriptPath, + String query, + String openKey, + String graphSchema, + String model, + String ernieClientId, + String ernieClientSecret) { + List argsList = new ArrayList<>(); + argsList.add(this.configValue(HubbleOptions.LANGCHAIN_PYTHON_PATH)); + argsList.add(pythonScriptPath); + argsList.add("--query"); + argsList.add(query); + argsList.add("--graph_schema"); + argsList.add(StringUtils.defaultString(graphSchema)); + if (WENXIN_4_MODEL.equals(model)) { + if (ernieClientSecret != null) { + argsList.add("--ernie_client_secret"); + argsList.add(ernieClientSecret); + } + if (ernieClientId != null) { + argsList.add("--ernie_client_id"); + argsList.add(ernieClientId); + } + } else if (GPT_4_MODEL.equals(model)) { + if (openKey != null) { + argsList.add("--open_key"); + argsList.add(openKey); + } + } + argsList.add("--model"); + argsList.add(model); + return argsList.toArray(new String[argsList.size()]); + } + + private T configValue(ConfigOption option) { + if (this.config == null) { + return option.defaultValue(); + } + return this.config.get(option); + } + + private void checkParamsValid(GremlinQuery query) { + Ex.check(!StringUtils.isEmpty(query.getContent()), + "common.param.cannot-be-null-or-empty", + "gremlin-query.content"); + Ex.check(query.getContent().length() <= GremlinController.CONTENT_LENGTH_LIMIT, + "gremlin.statement.exceed-limit", GremlinController.CONTENT_LENGTH_LIMIT); + } + + private HashMap getBigModelSchema(List vertexLabels, + List edgeLabels) { + List vertexLabelVoList = Lists.newArrayList(); + List edgeLabelVoList = Lists.newArrayList(); + List relationshipsList = Lists.newArrayList(); + if (CollectionUtils.isNotEmpty(vertexLabels)) { + for (VertexLabel vertexLabel : vertexLabels) { + VertexLabelVo vertexLabelVo = new VertexLabelVo(); + vertexLabelVo.setName(vertexLabel.name()); + vertexLabelVo.setPrimaryKeys(vertexLabel.primaryKeys()); + vertexLabelVo.setProperties(Lists.newArrayList()); + vertexLabelVo.getProperties().addAll(vertexLabel.properties()); + + vertexLabelVoList.add(vertexLabelVo); + } + } + + if (CollectionUtils.isNotEmpty(edgeLabels)) { + for (EdgeLabel edgeLabel : edgeLabels) { + EdgeLabelVo edgeLabelVo = new EdgeLabelVo(); + edgeLabelVo.setName(edgeLabel.name()); + edgeLabelVo.setProperties(Lists.newArrayList()); + edgeLabelVo.getProperties().addAll(edgeLabel.properties()); + + edgeLabelVoList.add(edgeLabelVo); + + relationshipsList.add(Relationship.getRelation(edgeLabel.sourceLabel(), + edgeLabel.name(), edgeLabel.targetLabel())); + } + } + + HashMap schema = new HashMap(); + schema.put("Node properties", vertexLabelVoList); + schema.put("Edge properties", edgeLabelVoList); + schema.put("Relationships", relationshipsList); + return schema; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + static class RequestLangChainParams { + @JsonProperty("python_path") + private String pythonPath = "python"; + + @JsonProperty("file_name") + private String fileName = DEFAULT_PYTHON_FILE; + + @JsonProperty("query") + private String query; + + @JsonProperty("open_key") + private String openKey; + + + @JsonProperty("graph_schema") + private String graphSchema = ""; + + @JsonProperty("ernie_client_secret") + private String ernieClientSecret; + + @JsonProperty("ernie_client_id") + private String ernieClientId; + + @JsonProperty("model") + private String model = "wenxin4"; + + @JsonProperty("username") + private String userName; + + @JsonProperty("password") + private String password; + + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + static class ResponseLangChain { + @JsonProperty("query") + private String query; + + @JsonProperty("gremlin") + private String gremlin; + } + + @Data + static class VertexLabelVo { + + @JsonProperty("name") + private String name; + + @JsonProperty("primary_keys") + private List primaryKeys; + + @JsonProperty("properties") + private List properties; + } + + @Data + static class EdgeLabelVo { + @JsonProperty("name") + private String name; + + @JsonProperty("properties") + private List properties; + } + + static class Relationship { + public static String getRelation(String sourceLabel, String name, String targetLabel) { + return sourceLabel + "--" + name + "-->" + targetLabel; + + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java index 5614334e1..a9642e0ac 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java @@ -18,20 +18,20 @@ package org.apache.hugegraph.controller.load; -import java.util.List; -import java.util.Set; - +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.enums.JobStatus; -import org.apache.hugegraph.entity.load.EdgeMapping; -import org.apache.hugegraph.entity.load.ElementMapping; import org.apache.hugegraph.entity.load.FileMapping; import org.apache.hugegraph.entity.load.FileSetting; +import org.apache.hugegraph.entity.load.ElementMapping; import org.apache.hugegraph.entity.load.JobManager; import org.apache.hugegraph.entity.load.LoadParameter; import org.apache.hugegraph.entity.load.VertexMapping; +import org.apache.hugegraph.entity.load.EdgeMapping; import org.apache.hugegraph.entity.schema.EdgeLabelEntity; import org.apache.hugegraph.entity.schema.VertexLabelEntity; import org.apache.hugegraph.exception.ExternalException; @@ -54,14 +54,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.baomidou.mybatisplus.core.metadata.IPage; - -import lombok.extern.log4j.Log4j2; +import java.util.List; +import java.util.Set; @Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + - "graph-connections/{connId}/job-manager/{jobId}/file-mappings") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/job-manager/{jobId}/file-mappings") public class FileMappingController extends BaseController { @Autowired @@ -74,7 +73,8 @@ public class FileMappingController extends BaseController { private JobManagerService jobService; @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam(name = "page_no", required = false, @@ -84,12 +84,15 @@ public IPage list(@PathVariable("connId") int connId, required = false, defaultValue = "10") int pageSize) { - return this.service.list(connId, jobId, pageNo, pageSize); + return this.service.list(graphSpace, graph, jobId, pageNo, pageSize); } @GetMapping("{id}") - public FileMapping get(@PathVariable("id") int id) { - FileMapping mapping = this.service.get(id); + public FileMapping get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @PathVariable("id") int id) { + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } @@ -97,8 +100,11 @@ public FileMapping get(@PathVariable("id") int id) { } @DeleteMapping("{id}") - public void delete(@PathVariable("id") int id) { - FileMapping mapping = this.service.get(id); + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @PathVariable("id") int id) { + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } @@ -108,15 +114,21 @@ public void delete(@PathVariable("id") int id) { } @DeleteMapping - public void clear() { - List mappings = this.service.listAll(); + public void clear(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId) { + List mappings = this.service.listByJob(graphSpace, graph, + jobId); for (FileMapping mapping : mappings) { this.service.remove(mapping.getId()); } } @PostMapping("{id}/file-setting") - public FileMapping fileSetting(@PathVariable("id") int id, + public FileMapping fileSetting(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @PathVariable("id") int id, @RequestBody FileSetting newEntity) { Ex.check(!StringUtils.isEmpty(newEntity.getDelimiter()), "load.file-mapping.file-setting.delimiter-cannot-be-empty"); @@ -129,7 +141,7 @@ public FileMapping fileSetting(@PathVariable("id") int id, Ex.check(!StringUtils.isEmpty(newEntity.getSkippedLine()), "load.file-mapping.file-setting.skippedline-cannot-be-empty"); - FileMapping mapping = this.service.get(id); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } @@ -145,14 +157,17 @@ public FileMapping fileSetting(@PathVariable("id") int id, } @PostMapping("{id}/vertex-mappings") - public FileMapping addVertexMapping(@PathVariable("connId") int connId, + public FileMapping addVertexMapping(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, @PathVariable("id") int id, @RequestBody VertexMapping newEntity) { - FileMapping mapping = this.service.get(id); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } - this.checkVertexMappingValid(connId, newEntity, mapping); + HugeClient client = this.authClient(graphSpace, graph); + this.checkVertexMappingValid(client, newEntity, mapping); newEntity.setId(HubbleUtil.generateSimpleId()); mapping.getVertexMappings().add(newEntity); @@ -161,15 +176,18 @@ public FileMapping addVertexMapping(@PathVariable("connId") int connId, } @PutMapping("{id}/vertex-mappings/{vmid}") - public FileMapping updateVertexMapping(@PathVariable("connId") int connId, + public FileMapping updateVertexMapping(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, @PathVariable("id") int id, @PathVariable("vmid") String vmId, @RequestBody VertexMapping newEntity) { - FileMapping mapping = this.service.get(id); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } - this.checkVertexMappingValid(connId, newEntity, mapping); + HugeClient client = this.authClient(graphSpace, graph); + this.checkVertexMappingValid(client, newEntity, mapping); VertexMapping vertexMapping = mapping.getVertexMapping(vmId); Ex.check(vertexMapping != null, @@ -184,9 +202,13 @@ public FileMapping updateVertexMapping(@PathVariable("connId") int connId, } @DeleteMapping("{id}/vertex-mappings/{vmid}") - public FileMapping deleteVertexMapping(@PathVariable("id") int id, + public FileMapping deleteVertexMapping( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @PathVariable("id") int id, @PathVariable("vmid") String vmid) { - FileMapping mapping = this.service.get(id); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } @@ -195,21 +217,24 @@ public FileMapping deleteVertexMapping(@PathVariable("id") int id, boolean removed = mapping.getVertexMappings().remove(vertexMapping); if (!removed) { throw new ExternalException( - "load.file-mapping.vertex-mapping.not-exist.id", vmid); + "load.file-mapping.vertex-mapping.not-exist.id", vmid); } this.service.update(mapping); return mapping; } @PostMapping("{id}/edge-mappings") - public FileMapping addEdgeMapping(@PathVariable("connId") int connId, + public FileMapping addEdgeMapping(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, @PathVariable("id") int id, @RequestBody EdgeMapping newEntity) { - FileMapping mapping = this.service.get(id); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } - this.checkEdgeMappingValid(connId, newEntity, mapping); + HugeClient client = this.authClient(graphSpace, graph); + this.checkEdgeMappingValid(client, newEntity, mapping); newEntity.setId(HubbleUtil.generateSimpleId()); mapping.getEdgeMappings().add(newEntity); @@ -218,15 +243,18 @@ public FileMapping addEdgeMapping(@PathVariable("connId") int connId, } @PutMapping("{id}/edge-mappings/{emid}") - public FileMapping updateEdgeMapping(@PathVariable("connId") int connId, + public FileMapping updateEdgeMapping(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, @PathVariable("id") int id, @PathVariable("emid") String emId, @RequestBody EdgeMapping newEntity) { - FileMapping mapping = this.service.get(id); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } - this.checkEdgeMappingValid(connId, newEntity, mapping); + HugeClient client = this.authClient(graphSpace, graph); + this.checkEdgeMappingValid(client, newEntity, mapping); EdgeMapping edgeMapping = mapping.getEdgeMapping(emId); Ex.check(edgeMapping != null, @@ -241,9 +269,13 @@ public FileMapping updateEdgeMapping(@PathVariable("connId") int connId, } @DeleteMapping("{id}/edge-mappings/{emid}") - public FileMapping deleteEdgeMapping(@PathVariable("id") int id, + public FileMapping deleteEdgeMapping( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @PathVariable("id") int id, @PathVariable("emid") String emid) { - FileMapping mapping = this.service.get(id); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, id); if (mapping == null) { throw new ExternalException("load.file-mapping.not-exist.id", id); } @@ -252,7 +284,7 @@ public FileMapping deleteEdgeMapping(@PathVariable("id") int id, boolean removed = mapping.getEdgeMappings().remove(edgeMapping); if (!removed) { throw new ExternalException( - "load.file-mapping.edge-mapping.not-exist.id", emid); + "load.file-mapping.edge-mapping.not-exist.id", emid); } this.service.update(mapping); return mapping; @@ -263,9 +295,13 @@ public FileMapping deleteEdgeMapping(@PathVariable("id") int id, * in actually */ @PostMapping("load-parameter") - public void loadParameter(@RequestBody LoadParameter newEntity) { + public void loadParameter(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @RequestBody LoadParameter newEntity) { this.checkLoadParameter(newEntity); - List mappings = this.service.listAll(); + List mappings = this.service.listByJob(graphSpace, graph, + jobId); for (FileMapping mapping : mappings) { LoadParameter oldEntity = mapping.getLoadParameter(); LoadParameter entity = this.mergeEntity(oldEntity, newEntity); @@ -275,8 +311,10 @@ public void loadParameter(@RequestBody LoadParameter newEntity) { } @PutMapping("next-step") - public JobManager nextStep(@PathVariable("jobId") int jobId) { - JobManager jobEntity = this.jobService.get(jobId); + public JobManager nextStep(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId) { + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); Ex.check(jobEntity.getJobStatus() == JobStatus.MAPPING, "job.manager.status.unexpected", @@ -286,10 +324,11 @@ public JobManager nextStep(@PathVariable("jobId") int jobId) { return jobEntity; } - private void checkVertexMappingValid(int connId, VertexMapping vertexMapping, + private void checkVertexMappingValid(HugeClient client, + VertexMapping vertexMapping, FileMapping fileMapping) { VertexLabelEntity vl = this.vlService.get(vertexMapping.getLabel(), - connId); + client); Ex.check(!vl.getIdStrategy().isAutomatic(), "load.file-mapping.vertex.automatic-id-unsupported"); @@ -305,24 +344,24 @@ private void checkVertexMappingValid(int connId, VertexMapping vertexMapping, vl.getPrimaryKeys().size(), "load.file-mapping.vertex.id-fields-should-same-size-pks"); Ex.check(!CollectionUtils.containsAny( - vertexMapping.fieldMappingToMap().values(), - vl.getPrimaryKeys()), + vertexMapping.fieldMappingToMap().values(), + vl.getPrimaryKeys()), "load.file-mapping.vertex.mapping-fields-cannot-contains-pk"); } else { Ex.check(vertexMapping.getIdFields().size() == 1, "load.file-mapping.vertex.id-fields-should-only-one"); } Ex.check(CollectionUtil.allUnique( - vertexMapping.fieldMappingToMap().values()), + vertexMapping.fieldMappingToMap().values()), "load.file-mapping.mapping-fields-should-no-duplicate"); this.checkMappingValid(vertexMapping, fileMapping); } - private void checkEdgeMappingValid(int connId, EdgeMapping edgeMapping, + private void checkEdgeMappingValid(HugeClient client, EdgeMapping edgeMapping, FileMapping fileMapping) { - EdgeLabelEntity el = this.elService.get(edgeMapping.getLabel(), connId); - VertexLabelEntity source = this.vlService.get(el.getSourceLabel(), connId); - VertexLabelEntity target = this.vlService.get(el.getTargetLabel(), connId); + EdgeLabelEntity el = this.elService.get(edgeMapping.getLabel(), client); + VertexLabelEntity source = this.vlService.get(el.getSourceLabel(), client); + VertexLabelEntity target = this.vlService.get(el.getTargetLabel(), client); Ex.check(!source.getIdStrategy().isAutomatic(), "load.file-mapping.vertex.automatic-id-unsupported"); Ex.check(!target.getIdStrategy().isAutomatic(), @@ -341,7 +380,7 @@ private void checkEdgeMappingValid(int connId, EdgeMapping edgeMapping, "load.file-mapping.edge.target-fields-should-in-column-names", edgeMapping.getTargetFields(), columnNames); Ex.check(CollectionUtil.allUnique( - edgeMapping.fieldMappingToMap().values()), + edgeMapping.fieldMappingToMap().values()), "load.file-mapping.mapping-fields-should-no-duplicate"); this.checkMappingValid(edgeMapping, fileMapping); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java index d019549a4..8efac6f53 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java @@ -23,17 +23,31 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.hugegraph.controller.BaseController; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.entity.enums.FileMappingStatus; @@ -41,6 +55,7 @@ import org.apache.hugegraph.entity.load.FileMapping; import org.apache.hugegraph.entity.load.FileUploadResult; import org.apache.hugegraph.entity.load.JobManager; +import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.exception.InternalException; import org.apache.hugegraph.options.HubbleOptions; import org.apache.hugegraph.service.load.FileMappingService; @@ -49,23 +64,14 @@ import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.FileUtil; import org.apache.hugegraph.util.HubbleUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; import lombok.extern.log4j.Log4j2; @Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/job-manager/{jobId}/upload-file") -public class FileUploadController { +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/job-manager/{jobId}/upload-file") +public class FileUploadController extends BaseController { @Autowired private HugeConfig config; @@ -75,10 +81,14 @@ public class FileUploadController { private JobManagerService jobService; @GetMapping("token") - public Map fileToken(@PathVariable("connId") int connId, + public Map fileToken( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("names") List fileNames) { + Ex.check(this.jobService.get(graphSpace, graph, jobId) != null, + "job-manager.not-exist.id", jobId); Ex.check(CollectionUtil.allUnique(fileNames), "load.upload.file.duplicate-name"); Map tokens = new HashMap<>(); @@ -94,7 +104,8 @@ public Map fileToken(@PathVariable("connId") int connId, } @PostMapping - public FileUploadResult upload(@PathVariable("connId") int connId, + public FileUploadResult upload(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("file") MultipartFile file, @RequestParam("name") String fileName, @@ -107,16 +118,17 @@ public FileUploadResult upload(@PathVariable("connId") int connId, this.checkTotalAndIndexValid(total, index); this.checkFileNameValid(fileName); this.checkFileNameMatchToken(fileName, token); - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Long sourceFileSize = this.resolveSourceFileSize(file, fileSize, total, index); - this.checkFileValid(jobId, jobEntity, file, fileName); + this.checkFileValid(graphSpace, graph, jobId, jobEntity, file, fileName); if (jobEntity.getJobStatus() == JobStatus.DEFAULT) { jobEntity.setJobStatus(JobStatus.UPLOADING); this.jobService.update(jobEntity); } // Ensure location exist and generate file path - String filePath = this.generateFilePath(connId, jobId, fileName); + String filePath = this.generateFilePath(graphSpace, graph, jobId, + fileName); // Check this file deleted before ReadWriteLock lock = this.uploadingTokenLocks().get(token); FileUploadResult result; @@ -133,16 +145,18 @@ public FileUploadResult upload(@PathVariable("connId") int connId, try { FileMapping reservedMapping; synchronized (this.service) { - reservedMapping = this.reserveUploadQuota(connId, jobId, + reservedMapping = this.reserveUploadQuota(graphSpace, graph, jobId, fileName, filePath, sourceFileSize); } - result = this.service.uploadFile(file, index, filePath); + result = this.service.uploadFile(file, fileName, index, filePath); if (result.getStatus() == FileUploadResult.Status.FAILURE) { return result; } synchronized (this.service) { - FileMapping mapping = this.service.get(connId, jobId, fileName); + // Verify the existence of fragmented files + FileMapping mapping = this.service.get(graphSpace, graph, jobId, + fileName); if (mapping == null) { mapping = reservedMapping; } @@ -160,7 +174,8 @@ public FileUploadResult upload(@PathVariable("connId") int connId, this.service.update(mapping); return result; } - JobManager currentJob = this.jobService.get(jobId); + JobManager currentJob = this.jobService.get(graphSpace, graph, + jobId); long actualFileSize; try { actualFileSize = this.resolveUploadedFileSize( @@ -203,18 +218,20 @@ public FileUploadResult upload(@PathVariable("connId") int connId, } @DeleteMapping - public Boolean delete(@PathVariable("connId") int connId, + public Boolean delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("name") String fileName, @RequestParam("token") String token) { this.checkFileNameValid(fileName); - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); Ex.check(jobEntity.getJobStatus() == JobStatus.UPLOADING || jobEntity.getJobStatus() == JobStatus.MAPPING || jobEntity.getJobStatus() == JobStatus.SETTING, "deleted.file.no-permission"); - FileMapping mapping = this.service.get(connId, jobId, fileName); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, + fileName); Ex.check(mapping != null, "load.file-mapping.not-exist.name", fileName); ReadWriteLock lock = this.uploadingTokenLocks().get(token); @@ -240,8 +257,10 @@ public Boolean delete(@PathVariable("connId") int connId, } @PutMapping("next-step") - public JobManager nextStep(@PathVariable("jobId") int jobId) { - JobManager jobEntity = this.jobService.get(jobId); + public JobManager nextStep(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId) { + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); Ex.check(jobEntity.getJobStatus() == JobStatus.UPLOADING, "job.manager.status.unexpected", @@ -262,6 +281,10 @@ private void checkTotalAndIndexValid(int total, int index) { if (index < 0) { throw new InternalException("The request params 'index' must >= 0"); } + if (index >= total) { + throw new InternalException("The request params 'index' must < " + + "'total'"); + } } private void checkFileNameMatchToken(String fileName, String token) { @@ -279,8 +302,9 @@ private void checkFileNameValid(String fileName) { "load.upload.file.name.invalid"); } - private void checkFileValid(int jobId, JobManager jobEntity, - MultipartFile file, String fileName) { + private void checkFileValid(String graphSpace, String graph, int jobId, + JobManager jobEntity, MultipartFile file, + String fileName) { Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); Ex.check(jobEntity.getJobStatus() == JobStatus.DEFAULT || jobEntity.getJobStatus() == JobStatus.UPLOADING || @@ -293,19 +317,29 @@ private void checkFileValid(int jobId, JobManager jobEntity, log.debug("File content type: {}", file.getContentType()); String format = FilenameUtils.getExtension(fileName); - List formatWhiteList = this.config.get( - HubbleOptions.UPLOAD_FILE_FORMAT_LIST); - Ex.check(formatWhiteList.contains(format), + Ex.check(StringUtils.isNotBlank(format), "load.upload.file.format.unsupported"); + List formatWhiteList = this.config.get( + HubbleOptions.UPLOAD_FILE_FORMAT_LIST); + String normalizedFormat = format.toLowerCase(Locale.ROOT); + boolean supported = formatWhiteList != null && + formatWhiteList.stream() + .filter(StringUtils::isNotBlank) + .map(String::trim) + .map(item -> item.toLowerCase(Locale.ROOT)) + .anyMatch(normalizedFormat::equals); + Ex.check(supported, "load.upload.file.format.unsupported"); } - private FileMapping reserveUploadQuota(int connId, int jobId, + private FileMapping reserveUploadQuota(String graphSpace, String graph, + int jobId, String fileName, String filePath, Long sourceFileSize) { - JobManager currentJob = this.jobService.get(jobId); + JobManager currentJob = this.jobService.get(graphSpace, graph, jobId); Ex.check(currentJob != null, "job-manager.not-exist.id", jobId); - FileMapping mapping = this.service.get(connId, jobId, fileName); + FileMapping mapping = this.service.get(graphSpace, graph, jobId, + fileName); Ex.check(mapping == null || mapping.getFileStatus() == FileMappingStatus.UPLOADING, "load.upload.file.existed", fileName); @@ -319,7 +353,7 @@ private FileMapping reserveUploadQuota(int connId, int jobId, reservedUploadingSize); if (mapping == null) { - mapping = new FileMapping(connId, fileName, filePath); + mapping = new FileMapping(graphSpace, graph, fileName, filePath); mapping.setJobId(jobId); this.fillUploadingReservation(mapping, reservedFileSize); this.service.save(mapping); @@ -356,13 +390,13 @@ private void checkFileSizeLimit(long fileSize, long currentJobSize, Ex.check(fileSize > 0L, "load.upload.file.cannot-be-empty"); long singleFileSizeLimit = this.config.get( - HubbleOptions.UPLOAD_SINGLE_FILE_SIZE_LIMIT); + HubbleOptions.UPLOAD_SINGLE_FILE_SIZE_LIMIT); Ex.check(fileSize <= singleFileSizeLimit, "load.upload.file.exceed-single-size", FileUtils.byteCountToDisplaySize(singleFileSizeLimit)); long totalFileSizeLimit = this.config.get( - HubbleOptions.UPLOAD_TOTAL_FILE_SIZE_LIMIT); + HubbleOptions.UPLOAD_TOTAL_FILE_SIZE_LIMIT); long totalReservedSize = this.safeAdd(this.safeAdd(fileSize, currentJobSize), reservedUploadingSize); @@ -445,14 +479,20 @@ private long safeAdd(long left, long right) { } } - private String generateFilePath(int connId, int jobId, String fileName) { + private String generateFilePath(String graphSpace, String graph, int jobId, + String fileName) { String location = this.config.get(HubbleOptions.UPLOAD_FILE_LOCATION); - String path = Paths.get(CONN_PREIFX + connId, JOB_PREIFX + jobId) - .toString(); + String path = Paths.get(CONN_PREIFX + graphSpace + "-" + graph, + JOB_PREIFX + jobId).toString(); this.ensureLocationExist(location, path); // Before merge: upload-files/conn-1/verson_person.csv/part-1 // After merge: upload-files/conn-1/file-mapping-1/verson_person.csv - return Paths.get(location, path, fileName).toString(); + Path root = Paths.get(location, path).normalize(); + Path filePathObj = root.resolve(fileName).normalize(); + if (!filePathObj.startsWith(root)) { + throw new ExternalException("load.upload.file.name.invalid"); + } + return filePathObj.toString(); } private void ensureLocationExist(String location, String connPath) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java index 12ccaa414..1db3ca327 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java @@ -18,9 +18,8 @@ package org.apache.hugegraph.controller.load; -import java.util.ArrayList; -import java.util.List; - +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.common.Response; @@ -48,13 +47,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.baomidou.mybatisplus.core.metadata.IPage; - -import lombok.extern.log4j.Log4j2; +import java.util.ArrayList; +import java.util.List; @Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/job-manager") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/job-manager") public class JobManagerController { private static final int LIMIT = 500; @@ -71,27 +70,29 @@ public JobManagerController(JobManagerService service) { } @PostMapping - public JobManager create(@PathVariable("connId") int connId, + public JobManager create(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody JobManager entity) { synchronized (this.service) { Ex.check(entity.getJobName().length() <= 48, "job.manager.job-name.reached-limit"); Ex.check(entity.getJobName() != null, () -> - Constant.COMMON_NAME_PATTERN.matcher( - entity.getJobName()).matches(), + Constant.COMMON_NAME_PATTERN.matcher( + entity.getJobName()).matches(), "job.manager.job-name.unmatch-regex"); Ex.check(entity.getJobRemarks().length() <= 200, "job.manager.job-remarks.reached-limit"); Ex.check(!StringUtils.isEmpty(entity.getJobRemarks()), () -> - Constant.COMMON_REMARK_PATTERN.matcher( - entity.getJobRemarks()).matches(), + Constant.COMMON_REMARK_PATTERN.matcher( + entity.getJobRemarks()).matches(), "job.manager.job-remarks.unmatch-regex"); Ex.check(this.service.count() < LIMIT, "job.manager.reached-limit", LIMIT); - if (this.service.getTask(entity.getJobName(), connId) != null) { + if (this.service.getTask(entity.getJobName(), graphSpace, graph) != null) { throw new InternalException("job.manager.job-name.repeated"); } - entity.setConnId(connId); + entity.setGraphSpace(graphSpace); + entity.setGraph(graph); entity.setJobStatus(JobStatus.DEFAULT); entity.setJobDuration(0L); entity.setJobSize(0L); @@ -103,13 +104,17 @@ public JobManager create(@PathVariable("connId") int connId, } @DeleteMapping("{id}") - public void delete(@PathVariable("id") int id) { - this.service.deleteJob(id); + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("id") int id) { + this.service.deleteJob(graphSpace, graph, id); } @GetMapping("{id}") - public JobManager get(@PathVariable("id") int id) { - JobManager job = this.service.get(id); + public JobManager get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("id") int id) { + JobManager job = this.service.get(graphSpace, graph, id); if (job == null) { throw new ExternalException("job.manager.not-exist.id", id); } @@ -117,49 +122,52 @@ public JobManager get(@PathVariable("id") int id) { } @GetMapping("ids") - public List list(@PathVariable("connId") int connId, + public List list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam("job_ids") List jobIds) { - return this.service.list(connId, jobIds); + return this.service.list(graphSpace, graph, jobIds); } @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam(name = "page_no", required = false, defaultValue = "1") - int pageNo, + int pageNo, @RequestParam(name = "page_size", required = false, defaultValue = "10") - int pageSize, + int pageSize, @RequestParam(name = "content", required = false, defaultValue = "") - String content) { - return this.service.list(connId, pageNo, pageSize, content); + String content) { + return this.service.list(graphSpace, graph, pageNo, pageSize, content); } @PutMapping("{id}") - public JobManager update(@PathVariable("connId") int connId, + public JobManager update(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") int id, @RequestBody JobManager newEntity) { Ex.check(newEntity.getJobName().length() <= 48, "job.manager.job-name.reached-limit"); Ex.check(newEntity.getJobName() != null, () -> - Constant.COMMON_NAME_PATTERN.matcher( - newEntity.getJobName()).matches(), + Constant.COMMON_NAME_PATTERN.matcher( + newEntity.getJobName()).matches(), "job.manager.job-name.unmatch-regex"); Ex.check(!StringUtils.isEmpty(newEntity.getJobRemarks()), () -> - Constant.COMMON_REMARK_PATTERN.matcher( - newEntity.getJobRemarks()).matches(), + Constant.COMMON_REMARK_PATTERN.matcher( + newEntity.getJobRemarks()).matches(), "job.manager.job-remarks.unmatch-regex"); // Check exist Job Manager with this id - JobManager entity = this.service.get(id); + JobManager entity = this.service.get(graphSpace, graph, id); if (entity == null) { throw new ExternalException("job-manager.not-exist.id", id); } if (!newEntity.getJobName().equals(entity.getJobName()) && - this.service.getTask(newEntity.getJobName(), connId) != null) { + this.service.getTask(newEntity.getJobName(), graphSpace, graph) != null) { throw new InternalException("job.manager.job-name.repeated"); } entity.setJobName(newEntity.getJobName()); @@ -169,9 +177,10 @@ public JobManager update(@PathVariable("connId") int connId, } @GetMapping("{id}/reason") - public Response reason(@PathVariable("connId") int connId, + public Response reason(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") int id) { - JobManager job = this.service.get(id); + JobManager job = this.service.get(graphSpace, graph, id); if (job == null) { throw new ExternalException("job.manager.not-exist.id", id); } @@ -182,7 +191,8 @@ public Response reason(@PathVariable("connId") int connId, int fileId = task.getFileId(); String reason = ""; if (task.getStatus() == LoadStatus.FAILED) { - FileMapping mapping = this.fmService.get(fileId); + FileMapping mapping = this.fmService.get(graphSpace, graph, id, + fileId); reason = this.taskService.readLoadFailedReason(mapping); } reasonResult.setTaskId(task.getJobId()); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java index b4862bf5b..edc0cd819 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java @@ -18,24 +18,26 @@ package org.apache.hugegraph.controller.load; -import java.util.ArrayList; -import java.util.List; - +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.GraphConnection; import org.apache.hugegraph.entity.enums.JobStatus; import org.apache.hugegraph.entity.load.FileMapping; import org.apache.hugegraph.entity.load.JobManager; import org.apache.hugegraph.entity.load.LoadTask; import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.service.GraphConnectionService; +import org.apache.hugegraph.options.HubbleOptions; import org.apache.hugegraph.service.load.FileMappingService; import org.apache.hugegraph.service.load.JobManagerService; import org.apache.hugegraph.service.load.LoadTaskService; import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.UrlUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -46,23 +48,23 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.baomidou.mybatisplus.core.metadata.IPage; - -import lombok.extern.log4j.Log4j2; +import java.util.ArrayList; +import java.util.List; @Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/job-manager/{jobId}/load-tasks") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/job-manager/{jobId}/load-tasks") public class LoadTaskController extends BaseController { private static final int LIMIT = 500; - @Autowired - private GraphConnectionService connService; @Autowired private FileMappingService fmService; @Autowired private JobManagerService jobService; + @Autowired + private HugeConfig config; private final LoadTaskService service; @@ -72,7 +74,8 @@ public LoadTaskController(LoadTaskService service) { } @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam(name = "page_no", required = false, @@ -82,18 +85,23 @@ public IPage list(@PathVariable("connId") int connId, required = false, defaultValue = "10") int pageSize) { - return this.service.list(connId, jobId, pageNo, pageSize); + return this.service.list(graphSpace, graph, jobId, pageNo, pageSize); } @GetMapping("ids") - public List list(@PathVariable("connId") int connId, + public List list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, @RequestParam("task_ids") List taskIds) { - return this.service.list(connId, taskIds); + return this.service.list(graphSpace, graph, jobId, taskIds); } @GetMapping("{id}") - public LoadTask get(@PathVariable("id") int id) { - LoadTask task = this.service.get(id); + public LoadTask get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @PathVariable("id") int id) { + LoadTask task = this.service.get(graphSpace, graph, jobId, id); if (task == null) { throw new ExternalException("load.task.not-exist.id", id); } @@ -101,25 +109,31 @@ public LoadTask get(@PathVariable("id") int id) { } @PostMapping - public LoadTask create(@PathVariable("connId") int connId, + public LoadTask create(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestBody LoadTask entity) { - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); Ex.check(jobEntity.getJobStatus() == JobStatus.SETTING, "load.task.create.no-permission"); synchronized (this.service) { Ex.check(this.service.count() < LIMIT, "load.task.reached-limit", LIMIT); - entity.setConnId(connId); + entity.setGraphSpace(graphSpace); + entity.setGraph(graph); + entity.setJobId(jobId); this.service.save(entity); } return entity; } @DeleteMapping("{id}") - public void delete(@PathVariable("id") int id) { - LoadTask task = this.service.get(id); + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("jobId") int jobId, + @PathVariable("id") int id) { + LoadTask task = this.service.get(graphSpace, graph, jobId, id); if (task == null) { throw new ExternalException("load.task.not-exist.id", id); } @@ -127,28 +141,44 @@ public void delete(@PathVariable("id") int id) { } @PostMapping("start") - public List start(@PathVariable("connId") int connId, + public List start(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("file_mapping_ids") List fileIds) { - GraphConnection connection = this.connService.get(connId); - if (connection == null) { - throw new ExternalException("graph-connection.not-exist.id", connId); + GraphConnection connection = new GraphConnection(); + + connection.setCluster(config.get(HubbleOptions.PD_CLUSTER)); + connection.setRouteType(config.get(HubbleOptions.ROUTE_TYPE)); + connection.setPdPeers(config.get(HubbleOptions.PD_PEERS)); + connection.setGraphSpace(graphSpace); + connection.setGraph(graph); + connection.setToken(this.getToken()); + connection.setUsername(this.getUser()); + connection.setPassword(this.getCredentialPassword()); + if (!config.get(HubbleOptions.PD_ENABLED)) { + UrlUtil.Host host = UrlUtil.parseHost(config.get(HubbleOptions.SERVER_URL)); + connection.setProtocol(host.getScheme()); + connection.setHost(host.getHost()); + connection.setPort(host.getPort()); } - JobManager jobEntity = this.jobService.get(jobId); + + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); Ex.check(jobEntity.getJobStatus() == JobStatus.SETTING, "load.task.start.no-permission"); boolean existError = false; try { List tasks = new ArrayList<>(); + HugeClient client = this.authClient(graphSpace, graph); for (Integer fileId : fileIds) { - FileMapping fileMapping = this.fmService.get(fileId); + FileMapping fileMapping = this.fmService.get(graphSpace, graph, + jobId, fileId); if (fileMapping == null) { throw new ExternalException("file-mapping.not-exist.id", fileId); } - tasks.add(this.service.start(connection, fileMapping)); + tasks.add(this.service.start(connection, fileMapping, client)); } return tasks; } catch (Exception e) { @@ -166,15 +196,14 @@ public List start(@PathVariable("connId") int connId, } @PostMapping("pause") - public LoadTask pause(@PathVariable("connId") int connId, + public LoadTask pause(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("task_id") int taskId) { - GraphConnection connection = this.connService.get(connId); - if (connection == null) { - throw new ExternalException("graph-connection.not-exist.id", connId); - } - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); + Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null, + "load.task.not-exist.id", taskId); Ex.check(jobEntity.getJobStatus() == JobStatus.LOADING, "load.task.pause.no-permission"); try { @@ -187,15 +216,14 @@ public LoadTask pause(@PathVariable("connId") int connId, } @PostMapping("resume") - public LoadTask resume(@PathVariable("connId") int connId, + public LoadTask resume(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("task_id") int taskId) { - GraphConnection connection = this.connService.get(connId); - if (connection == null) { - throw new ExternalException("graph-connection.not-exist.id", connId); - } - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); + Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null, + "load.task.not-exist.id", taskId); Ex.check(jobEntity.getJobStatus() == JobStatus.LOADING, "load.task.pause.no-permission"); try { @@ -208,15 +236,14 @@ public LoadTask resume(@PathVariable("connId") int connId, } @PostMapping("stop") - public LoadTask stop(@PathVariable("connId") int connId, + public LoadTask stop(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("task_id") int taskId) { - GraphConnection connection = this.connService.get(connId); - if (connection == null) { - throw new ExternalException("graph-connection.not-exist.id", connId); - } - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); + Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null, + "load.task.not-exist.id", taskId); Ex.check(jobEntity.getJobStatus() == JobStatus.LOADING, "load.task.pause.no-permission"); try { @@ -229,15 +256,14 @@ public LoadTask stop(@PathVariable("connId") int connId, } @PostMapping("retry") - public LoadTask retry(@PathVariable("connId") int connId, + public LoadTask retry(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @RequestParam("task_id") int taskId) { - GraphConnection connection = this.connService.get(connId); - if (connection == null) { - throw new ExternalException("graph-connection.not-exist.id", connId); - } - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); + Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null, + "load.task.not-exist.id", taskId); Ex.check(jobEntity.getJobStatus() == JobStatus.LOADING, "load.task.pause.no-permission"); try { @@ -250,17 +276,19 @@ public LoadTask retry(@PathVariable("connId") int connId, } @GetMapping("{id}/reason") - public Response reason(@PathVariable("connId") int connId, + public Response reason(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("jobId") int jobId, @PathVariable("id") int id) { - LoadTask task = this.service.get(id); + LoadTask task = this.service.get(graphSpace, graph, jobId, id); if (task == null) { throw new ExternalException("load.task.not-exist.id", id); } - JobManager jobEntity = this.jobService.get(jobId); + JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId); Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId); Integer fileId = task.getFileId(); - FileMapping mapping = this.fmService.get(fileId); + FileMapping mapping = this.fmService.get(graphSpace, graph, jobId, + fileId); String reason = this.service.readLoadFailedReason(mapping); return Response.builder() .status(Constant.STATUS_OK) diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/DashboardController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/DashboardController.java new file mode 100644 index 000000000..96344ab32 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/DashboardController.java @@ -0,0 +1,54 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.op; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.util.E; + +@RestController +@RequestMapping(Constant.API_VERSION + "dashboard") +public class DashboardController extends BaseController { + @Autowired + private HugeConfig config; + + @GetMapping + public Map listOperations() { + String address = config.get(HubbleOptions.DASHBOARD_ADDRESS); + E.checkArgument(StringUtils.isNotEmpty(address), + "Please set 'dashboard.address' in config file " + + "conf/hugegraph-hubble.properties."); + + Map result = new HashMap<>(); + result.put("address", address); + result.put("protocol", config.get(HubbleOptions.SERVER_PROTOCOL)); + return result; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/HStoreController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/HStoreController.java new file mode 100644 index 000000000..66cddcf75 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/HStoreController.java @@ -0,0 +1,167 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.op; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.service.space.HStoreService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.structure.space.HStoreNodeInfo; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.exception.ServerCapabilityUnavailableException; +import org.apache.hugegraph.exception.ServerException; + +@Log4j2 +@RestController +@RequestMapping(Constant.API_VERSION + "services/storage") +public class HStoreController extends BaseController { + + @Autowired + HStoreService hStoreService; + + @GetMapping + public Object listPage(@RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + + HugeClient client = this.authClient(null, null); + return hStoreService.listPage(client, pageNo, pageSize); + } + + @GetMapping("nodes/{id}") + public Object get(@PathVariable("id") String id) { + HugeClient client = this.authClient(null, null); + E.checkNotNull(id, "id"); + + List partitions = + client.hStoreManager().get(id).hStorePartitionInfoList(); + return ImmutableMap.of("shards", partitions); + } + + /** + * + * @return the status of sotre cluster + * "Cluster_OK": "正常", + * "Batch_import_Mode": "单副本入库模式" + * "Partition_Split": "数据分裂中" + * "Cluster_Not_Ready": "集群未就绪" + * "Cluster_Fault": "集群异常" + */ + @GetMapping("status") + public Object status() { + HugeClient client = this.authClient(null, null); + // TODO: Server 1.7.0 does not expose the /hstore/status REST API used + // by HugeClient. Keep this call real (do not synthesize health); remove + // this TODO only after Server provides and Hubble CI verifies it. + String status; + try { + status = client.hStoreManager().status(); + } catch (ServerException e) { + throw new ServerCapabilityUnavailableException( + "server.capability.hstore-status.unavailable", e); + } + + return ImmutableMap.of("status", status); + } + + /** + * Trigger the store cluster to start splitting + */ + @GetMapping("split") + public void triggerSplit() { + HugeClient client = this.authClient(null, null); + client.hStoreManager().startSplit(); + } + + /** + * startup list of servers to be respecified by params + * @param request {"nodes": ["node_id1", ]} + */ + @PostMapping("nodes/startup") + public void nodesStartup(@RequestBody Map> request) { + List nodes = request.getOrDefault("nodes", ImmutableList.of()); + + E.checkNotEmpty(nodes, "nodes"); + + HugeClient client = this.authClient(null, null); + + List successNodes = new ArrayList<>(); + + for (String nodeId: nodes) { + try { + client.hStoreManager().nodeStartup(nodeId); + } catch (RuntimeException e) { + log.info("startup hstore node({}) success", successNodes); + log.warn("startup hstore node({}) error", nodeId, e); + String msg = String.format("shutdown(%s) error: %s", nodeId, + e.getMessage()); + throw new ExternalException(msg, e); + } + + successNodes.add(nodeId); + } + } + + /** + * startup list of servers to be respecified by params + * @param request + */ + @PostMapping("nodes/shutdown") + public void nodesShutdown(@RequestBody Map> request) { + List nodes = request.getOrDefault("nodes", ImmutableList.of()); + + E.checkNotEmpty(nodes, "nodes"); + + HugeClient client = this.authClient(null, null); + + List successNodes = new ArrayList<>(); + + for (String nodeId: nodes) { + try { + client.hStoreManager().nodeShutdown(nodeId); + } catch (RuntimeException e) { + log.info("shutdown hstore node({}) success", successNodes); + log.warn("shutdown hstore node({}) error", nodeId, e); + String msg = String.format("shutdown(%s) error: %s", nodeId, + e.getMessage()); + throw new ExternalException(msg, e); + } + + successNodes.add(nodeId); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/K8sTokenController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/K8sTokenController.java new file mode 100644 index 000000000..ce878b46f --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/K8sTokenController.java @@ -0,0 +1,84 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.op; + +import java.io.File; +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 org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.exception.InternalException; +import com.google.common.collect.ImmutableMap; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; + +@Log4j2 +// SECURITY: This controller is intentionally not registered or mapped. Do not expose +// Kubernetes credentials through Hubble without a privileged authorization design. +public class K8sTokenController extends BaseController { + + @Autowired + private ApplicationArguments arguments; + + private Path fileDir() { + String[] args = this.arguments.getSourceArgs(); + if (args.length == 1) { + return new File(args[0]).getAbsoluteFile().getParentFile().toPath(); + } + + return null; + } + + public Object getK8sToken() { + + Path configDir = fileDir(); + + if (null == configDir) { + throw new InternalException("k8s.token.file.not-exist"); + } + + Path tokenFile = Paths.get(configDir.toString(), "k8s.token"); + + if (Files.exists(tokenFile)) { + try { + List lines = Files.readAllLines(tokenFile, + StandardCharsets.UTF_8); + return ImmutableMap.of("token", String.join("", lines)); + } catch (IOException e) { + log.error("Failed to read the Kubernetes token file", e); + } + } + + throw new InternalException("k8s.token.file.not-exist"); + } + + public Object getK8sToken1() { + + Path configDir = fileDir(); + if (configDir == null) { + throw new InternalException("k8s.token.directory.unavailable"); + } + return ImmutableMap.of("token", configDir.toString()); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/LogController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/LogController.java new file mode 100644 index 000000000..4ade014cb --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/LogController.java @@ -0,0 +1,102 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.op; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.google.common.collect.ImmutableMap; +import lombok.SneakyThrows; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.entity.op.LogEntity; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.service.op.LogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping(Constant.API_VERSION + "logs") +public class LogController extends BaseController { + @Autowired + LogService logService; + + @SneakyThrows + @GetMapping("services/list") + public Object listServices() { + List services = logService.listServices(); + + return ImmutableMap.of("services", services); + } + + @SneakyThrows + @GetMapping("hosts/list") + public Object listHosts() { + List hosts = logService.listHosts(); + + return ImmutableMap.of("hosts", hosts); + } + + @GetMapping("levels/list") + public Object listLevels() { + List levels = Arrays.asList(LogService.LEVELS); + + return ImmutableMap.of("levels", levels); + } + + @SneakyThrows + @PostMapping("query") + public IPage query(@RequestBody LogService.LogReq logReq) { + return logService.queryPage(logReq); + } + + @SneakyThrows + @PostMapping("export") + public void export(HttpServletResponse response, + @RequestBody LogService.LogReq logReq) { + String fileName = String.format("log.txt", logReq.startDatetime, + logReq.endDatetime); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/octet-stream"); + response.setHeader("Access-Control-Expose-Headers", + "Content-Disposition"); + response.setHeader("Content-Disposition", + "attachment;filename=" + fileName); + try { + OutputStream os = response.getOutputStream(); + for (LogEntity logEntity : logService.export(logReq)) { + os.write((logEntity.getMessage() + "\n") + .getBytes(StandardCharsets.UTF_8)); + } + os.close(); + } catch (IOException e) { + throw new InternalException("Log Write Error", e); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/MonitorController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/MonitorController.java new file mode 100644 index 000000000..948af6deb --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/MonitorController.java @@ -0,0 +1,57 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.op; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.util.E; + +@RestController +@RequestMapping(Constant.API_VERSION + "monitor") +public class MonitorController extends BaseController { + + @Autowired + private HugeConfig config; + + @GetMapping + public Object monitor() { + String monitorURL = null; + // Get monitor.url from system.env + monitorURL = System.getenv(HubbleOptions.MONITOR_URL.name()); + if (StringUtils.isEmpty(monitorURL)) { + // get monitor.url from file: hugegraph-hubble.properties + monitorURL = config.get(HubbleOptions.MONITOR_URL); + } + + E.checkArgument(StringUtils.isNotEmpty(monitorURL), + "Please set \"monitor.url\" in system environments " + + "or config file(hugegraph-hubble.properties)."); + + return ImmutableMap.of("url", monitorURL); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/PDController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/PDController.java new file mode 100644 index 000000000..a692e880b --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/PDController.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.op; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ServerCapabilityUnavailableException; +import org.apache.hugegraph.exception.ServerException; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; + +@RestController +@RequestMapping(Constant.API_VERSION + "pds") +public class PDController extends BaseController { + + @GetMapping("status") + public Object status() { + HugeClient client = this.authClient(null, null); + // TODO: Server 1.7.0 does not expose the /pd status REST API used by + // HugeClient. Keep this call real (do not synthesize health); remove + // this TODO only after Server provides and Hubble CI verifies it. + try { + return client.pdManager().status(); + } catch (ServerException e) { + throw new ServerCapabilityUnavailableException( + "server.capability.pd-status.unavailable", e); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ApplicationInfoController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ApplicationInfoController.java new file mode 100644 index 000000000..db620165b --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ApplicationInfoController.java @@ -0,0 +1,125 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.query; + +import org.apache.hugegraph.common.AppType; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.controller.saas.SaaSMetricsController; +import org.apache.hugegraph.entity.query.ApplicationInfo; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.query.ApplicationInfoService; +import org.apache.hugegraph.util.E; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping(Constant.API_VERSION + + "graphspaces/{graphspace}/graphs/{graph}/applications") +public class ApplicationInfoController extends BaseController { + @Autowired + private ApplicationInfoService appInfoService; + + @Autowired + private HugeConfig config; + + @PostMapping + public ApplicationInfo insertOrUpdateAppInfo( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ApplicationInfo appInfo) { + appInfo.setGraphName(join(config.get(HubbleOptions.IDC), graphSpace, graph)); + checkParams(appInfo, graphSpace); + appInfoService.insertOrUpdateAppInfo(appInfo); + return appInfo; + } + + @GetMapping + public List list( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph) { + String idc = config.get(HubbleOptions.IDC); + //query apps from database + List apps = + appInfoService.query(join(idc, graphSpace, graph)); + + // add three default apps + apps.add(SaaSMetricsController.defaultGremlinApp(idc, graphSpace, graph)); + apps.add(SaaSMetricsController.defaultVertexUpdateApp(idc, graphSpace, graph)); + apps.add(SaaSMetricsController.defaultEdgeUpdateApp(idc, graphSpace, graph)); + return apps; + } + + @GetMapping("/{appName}/{appType}") + public ApplicationInfo appInfo( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("appName") String appName, + @PathVariable("appType") String appType) { + String idc = config.get(HubbleOptions.IDC); + if (AppType.GENERAL.name().equals(appType) && "GREMLIN".equals(appName)) { + return SaaSMetricsController.defaultGremlinApp(idc, graphSpace, graph); + } + if (AppType.GENERAL.name().equals(appType) && "VERTEX-UPDATE".equals(appName)) { + return SaaSMetricsController.defaultVertexUpdateApp(idc, graphSpace, graph); + } + if (AppType.GENERAL.name().equals(appType) && "EDGE-UPDATE".equals(appName)) { + return SaaSMetricsController.defaultEdgeUpdateApp(idc, graphSpace, graph); + } + + List apps = + appInfoService.query(join(idc, graphSpace, graph), appName, appType); + E.checkState(apps != null && apps.size() == 1, + "Expect exactly one application info for %s/%s", + appName, appType); + return apps.get(0); + } + + @DeleteMapping("/{appName}/{appType}") + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("appName") String appName, + @PathVariable("appType") String appType) { + appInfoService.delete( + join(config.get(HubbleOptions.IDC), graphSpace, graph), appName, appType); + } + + private void checkParams(ApplicationInfo appInfo, String graphspace) { + E.checkArgument( + appInfo.getGraphName() != null && + appInfo.getAppType() != null && + appInfo.getAppName() != null && + appInfo.getCountQuery() != null && + appInfo.getDistributionQuery() != null, + "application info each filed can not be null!"); + } + + private static String join(String idc, String graphSpace, String graph) { + return String.join("-", idc, graphSpace, graph); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/CypherController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/CypherController.java new file mode 100644 index 000000000..6d6e357af --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/CypherController.java @@ -0,0 +1,145 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.query; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.enums.AsyncTaskStatus; +import org.apache.hugegraph.entity.enums.ExecuteStatus; +import org.apache.hugegraph.entity.enums.ExecuteType; +import org.apache.hugegraph.entity.query.ExecuteHistory; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.entity.query.GremlinResult; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.util.Ex; +import org.apache.hugegraph.util.HubbleUtil; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.StopWatch; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Log4j2 +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/cypher") +public class CypherController extends GremlinController { + @Autowired + private ExecuteHistoryService historyService; + + @Autowired + private QueryService queryService; + + @GetMapping + public GremlinResult execute(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "cypher") String query) { + this.checkParamsValid(query); + + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.CYPHER, query, + status, AsyncTaskStatus.UNKNOWN, + -1L, createTime); + this.historyService.save(history); + + StopWatch timer = StopWatch.createStarted(); + try { + HugeClient client = this.authClient(graphSpace, graph); + GremlinResult result = + this.queryService.executeCypherQuery(client, query); + status = ExecuteStatus.SUCCESS; + return result; + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + history.setFailureReason( + GremlinQueryController.GREMLIN_EXECUTION_FAILED); + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + + } + + @PostMapping("async-task") + public Map executeAsyncTask( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody GremlinQuery query) { + this.checkParamsValid(query.getContent()); + + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.ASYNC_TASK_RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.CYPHER_ASYNC, + query.getContent(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + + StopWatch timer = StopWatch.createStarted(); + long asyncId = 0L; + Map result = new HashMap<>(3); + try { + HugeClient client = this.authClient(graphSpace, graph); + asyncId = this.queryService.executeCypherAsyncTask(client, query.getContent()); + status = ExecuteStatus.ASYNC_TASK_SUCCESS; + result.put("task_id", asyncId); + result.put("execute_status", status); + return result; + } catch (Throwable e) { + status = ExecuteStatus.ASYNC_TASK_FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + history.setAsyncId(asyncId); + this.historyService.update(history); + } + } + + private void checkParamsValid(String query) { + Ex.check(!StringUtils.isEmpty(query), + "common.param.cannot-be-null-or-empty", + "gremlin-query.content"); + checkContentLength(query); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/EditElementHistoryController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/EditElementHistoryController.java new file mode 100644 index 000000000..ac008cfda --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/EditElementHistoryController.java @@ -0,0 +1,260 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.ElementEditHistory; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.entity.query.GremlinResult; +import org.apache.hugegraph.service.query.EditElementHistoryService; +import org.apache.hugegraph.structure.GraphElement; +import org.apache.hugegraph.structure.SchemaElement; +import org.apache.hugegraph.structure.graph.Edge; +import org.apache.hugegraph.structure.graph.Vertex; +import org.apache.hugegraph.structure.schema.EdgeLabel; +import org.apache.hugegraph.structure.schema.VertexLabel; +import org.apache.hugegraph.util.PageUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/edit-histories") +public class EditElementHistoryController extends BaseController { + + @Autowired + private EditElementHistoryService service; + + @Autowired + private GremlinQueryController gremlinService; + + /** + * 根据条件查询编辑历史记录 + * @param graphSpace 图空间 + * @param graph 图 + * @param optionPersons 操作人 + * @param optionTimeFrom 操作时间from + * @param optionTimeTo 操作时间to + * @param optionTypes 操作类型 + * @param elementId 元素id + * @param pageNo 页码 + * @param pageSize 每页大小 + * @return 返回分页后的编辑历史记录 + */ + @GetMapping("filter") + public IPage queryByConditions( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "option_person", + required = false, + defaultValue = "") String optionPersons, + @RequestParam(name = "option_time_from", + required = false, + defaultValue = "") String optionTimeFrom, + @RequestParam(name = "option_time_to", + required = false, + defaultValue = "") String optionTimeTo, + @RequestParam(name = "option_type", + required = false, + defaultValue = "") String optionTypes, + @RequestParam(name = "element_id", + required = false, + defaultValue = "") String elementId, + @RequestParam(name = "page_no", + required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", + required = false, + defaultValue = "10") int pageSize) { + boolean hasNoConditions = + hasNoConditions(optionPersons, optionTimeFrom, optionTimeTo, + optionTypes, elementId); + if (hasNoConditions) { + return this.service.list(graphSpace, graph, pageNo, pageSize); + } + + List optionPersonsList = + optionPersons.isEmpty() ? new ArrayList<>() : + Arrays.asList(optionPersons.split(",")); + + List optionTypesList = + optionTypes.isEmpty() ? new ArrayList<>() : + Arrays.asList(optionTypes.split(",")); + + return this.service.queryByConditions(graphSpace, graph, + optionPersonsList, + optionTypesList, + elementId, + optionTimeFrom, + optionTimeTo, + pageNo, pageSize); + } + + @GetMapping("gremlin") + public IPage queryByGremlin( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "page_no", + required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", + required = false, + defaultValue = "10") int pageSize, + @RequestParam(name = "gremlin_query", + required = true) String gremlinQuery) { + // FIXME: Push range/limit and an explicit total-count strategy to the server; + // collecting the complete Gremlin result defeats pagination and risks OOM. + PageUtil.checkPositivePage(pageNo, pageSize); + GremlinResult gremlin = + gremlinService.gremlin(graphSpace, graph, + new GremlinQuery(gremlinQuery)); + List elements = collectElements(gremlin); + if (elements.isEmpty()) { + return PageUtil.page(new ArrayList<>(), pageNo, pageSize); + } + + List subElements = getSubList(elements, pageNo, pageSize); + + List elementIds = new ArrayList<>(); + subElements.forEach(e -> elementIds.add(e.id().toString())); + + Map histories = + service.queryByElementIds(graphSpace, graph, elementIds); + + List mergedElements = + mergeEditHistory(this.authClient(graphSpace, graph), histories, + subElements); + return PageUtil.newPage(mergedElements, pageNo, pageSize, elements.size()); + } + + private List mergeEditHistory(HugeClient client, + Map histories, + List elements) { + List mergedElements = new ArrayList<>(); + + Map vlMap = new HashMap<>(); + client.schema().getVertexLabels() + .forEach(vl -> vlMap.put(vl.name(), vl)); + + Map elMap = new HashMap<>(); + client.schema().getEdgeLabels() + .forEach(el -> elMap.put(el.name(), el)); + + for (GraphElement e : elements) { + Element.ElementBuilder eBuilder = + Element.builder() + .element(e) + .propertyNum(e.properties().size()); + if (e instanceof Vertex) { + eBuilder.label(vlMap.get(e.label())); + } else if (e instanceof Edge) { + eBuilder.label(elMap.get(e.label())); + } + ElementEditHistory his = histories.get(e.id().toString()); + if (his != null) { + eBuilder.lastContent(his.getContent()) + .lastOptionTime(his.getOptionTime()) + .lastOptionPerson(his.getOptionPerson()) + .lastOptionType(his.getOptionType()); + } + mergedElements.add(eBuilder.build()); + } + return mergedElements; + } + + private List getSubList(List elements, + int pageNo, int pageSize) { + long offset = (long) (pageNo - 1) * pageSize; + int start = (int) Math.min(offset, elements.size()); + int end = (int) Math.min(offset + pageSize, elements.size()); + return elements.subList(start, end); + } + + private List collectElements(GremlinResult gremlin) { + List elements = new ArrayList<>(); + if (gremlin.getType().isEmpty()) { + return elements; + } + + for (Object obj : gremlin.getJsonView().getData()) { + if (obj instanceof Vertex) { + Vertex v = (Vertex) obj; + elements.add(v); + } else if (obj instanceof Edge) { + Edge e = (Edge) obj; + elements.add(e); + } + } + return elements; + } + + private boolean hasNoConditions(String optionPersons, + String optionTimeFrom, + String optionTimeTo, + String optionTypes, + String elementId) { + return optionPersons.isEmpty() && optionTimeFrom.isEmpty() && + optionTimeTo.isEmpty() && optionTypes.isEmpty() && + elementId.isEmpty(); + } + + @Data + @Builder + public static class Element { + @JsonProperty("element") + private GraphElement element; + + @JsonProperty("label") + private SchemaElement label; + + @JsonProperty("property_num") + private int propertyNum; + + @JsonProperty("last_option_type") + private String lastOptionType; + + @JsonProperty("last_option_time") + private Date lastOptionTime; + + @JsonProperty("last_option_person") + private String lastOptionPerson; + + @JsonProperty("last_content") + private String lastContent; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java index 733028678..b7f1fe24c 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java @@ -18,7 +18,9 @@ package org.apache.hugegraph.controller.query; +import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.query.ExecuteHistory; import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.service.query.ExecuteHistoryService; @@ -30,17 +32,18 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.baomidou.mybatisplus.core.metadata.IPage; - @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/execute-histories") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/execute-histories") public class ExecuteHistoryController extends GremlinController { @Autowired private ExecuteHistoryService service; @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "type") int type, @RequestParam(name = "page_no", required = false, defaultValue = "1") @@ -48,24 +51,33 @@ public IPage list(@PathVariable("connId") int connId, @RequestParam(name = "page_size", required = false, defaultValue = "10") - int pageSize) { - return this.service.list(connId, pageNo, pageSize); + int pageSize, + @RequestParam(name = "text2gremlin", + required = false, + defaultValue = "false") + boolean text2Gremlin) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.list(client, type, pageNo, pageSize, text2Gremlin); } @GetMapping("{id}") - public ExecuteHistory get(@PathVariable("connId") int connId, + public ExecuteHistory get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") int id) { - return this.service.get(connId, id); + HugeClient client = this.authClient(graphSpace, graph); + return this.service.get(client, id); } @DeleteMapping("{id}") - public ExecuteHistory delete(@PathVariable("connId") int connId, + public ExecuteHistory delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") int id) { - ExecuteHistory oldEntity = this.service.get(connId, id); + HugeClient client = this.authClient(graphSpace, graph); + ExecuteHistory oldEntity = this.service.get(client, id); if (oldEntity == null) { throw new ExternalException("execute-history.not-exist.id", id); } - this.service.remove(connId, id); + this.service.remove(client, id); return oldEntity; } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java index dd515271a..8f66c8590 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java @@ -18,13 +18,9 @@ package org.apache.hugegraph.controller.query; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.enums.ExecuteType; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.common.Constant; -import org.apache.hugegraph.entity.query.GremlinCollection; -import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.service.query.GremlinCollectionService; -import org.apache.hugegraph.util.Ex; -import org.apache.hugegraph.util.HubbleUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -36,10 +32,17 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.entity.query.GremlinCollection; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.service.query.GremlinCollectionService; +import org.apache.hugegraph.util.Ex; +import org.apache.hugegraph.util.HubbleUtil; import com.baomidou.mybatisplus.core.metadata.IPage; @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/gremlin-collections") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/gremlin-collections") public class GremlinCollectionController extends GremlinController { private static final int LIMIT = 100; @@ -52,7 +55,10 @@ public GremlinCollectionController(GremlinCollectionService service) { } @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "type") + String type, @RequestParam(name = "content", required = false) String content, @@ -85,7 +91,16 @@ public IPage list(@PathVariable("connId") int connId, "common.time-order.invalid", timeOrder); timeOrderAsc = ORDER_ASC.equals(timeOrder); } - return this.service.list(connId, content, nameOrderAsc, timeOrderAsc, + + if (!StringUtils.isEmpty(type)) { + Ex.check(ExecuteType.GREMLIN.name().equals(type) || + ExecuteType.CYPHER.name().equals(type) || + ExecuteType.ALGORITHM.name().equals(type), + "common.type.invalid", type); + } + + HugeClient client = this.authClient(graphSpace, graph); + return this.service.list(client, content, type, nameOrderAsc, timeOrderAsc, pageNo, pageSize); } @@ -95,10 +110,12 @@ public GremlinCollection get(@PathVariable("id") int id) { } @PostMapping - public GremlinCollection create(@PathVariable("connId") int connId, + public GremlinCollection create(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody GremlinCollection newEntity) { this.checkParamsValid(newEntity, true); - newEntity.setConnId(connId); + newEntity.setGraphSpace(graphSpace); + newEntity.setGraph(graph); newEntity.setCreateTime(HubbleUtil.nowDate()); this.checkEntityUnique(newEntity, true); // The service is an singleton object @@ -163,10 +180,17 @@ private void checkParamsValid(GremlinCollection newEntity, private void checkEntityUnique(GremlinCollection newEntity, boolean creating) { - int connId = newEntity.getConnId(); + String graphSpace = newEntity.getGraphSpace(); + String graph = newEntity.getGraph(); String name = newEntity.getName(); + String type = newEntity.getType(); // NOTE: Full table scan may slow, it's better to use index - GremlinCollection oldEntity = this.service.getByName(connId, name); + GremlinCollection oldEntity = this.service.getByName(graphSpace, graph, + name, type); + Ex.check(newEntity.getType().equals(ExecuteType.GREMLIN.name()) || + newEntity.getType().equals(ExecuteType.CYPHER.name()) || + newEntity.getType().equals(ExecuteType.ALGORITHM.name()), + "gremlin-collection.type.invalid", newEntity.getType()); if (creating) { Ex.check(oldEntity == null, "gremlin-collection.exist.name", name); } else { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinController.java index 3af15c282..abbc3489f 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinController.java @@ -18,11 +18,11 @@ package org.apache.hugegraph.controller.query; -import java.util.regex.Pattern; - import org.apache.hugegraph.controller.BaseController; import org.apache.hugegraph.util.Ex; +import java.util.regex.Pattern; + public abstract class GremlinController extends BaseController { public static final Pattern CONTENT_PATTERN = Pattern.compile( diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinQueryController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinQueryController.java index 58f7fcc5d..ebdbee186 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinQueryController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinQueryController.java @@ -19,11 +19,24 @@ package org.apache.hugegraph.controller.query; import java.util.Date; -import java.util.Set; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; +import org.apache.hugegraph.driver.HugeClient; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.StopWatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.entity.enums.AsyncTaskStatus; import org.apache.hugegraph.entity.enums.ExecuteStatus; @@ -34,56 +47,110 @@ import org.apache.hugegraph.entity.query.GremlinResult; import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.service.query.ExecuteHistoryService; -import org.apache.hugegraph.service.query.GremlinQueryService; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.structure.gremlin.ResultSet; import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.HubbleUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; -import lombok.extern.log4j.Log4j2; - -@Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/gremlin-query") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/gremlin-query") public class GremlinQueryController extends GremlinController { - private static final Set CONDITION_OPERATORS = ImmutableSet.of( - "eq", "gt", "gte", "lt", "lte" - ); + static final String GREMLIN_EXECUTION_FAILED = + "GREMLIN_EXECUTION_FAILED"; + + private static final Logger LOG = + LoggerFactory.getLogger(GremlinQueryController.class); @Autowired - private GremlinQueryService queryService; + private QueryService queryService; @Autowired private ExecuteHistoryService historyService; + @GetMapping + public Map execute(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph) { + + HugeClient client = this.authGremlinClient(graphSpace, graph); + Map graphCount = new HashMap<>(); + graphCount.put("vertexcount", queryCount(client, "g.V().count()")); + graphCount.put("edgecount", queryCount(client, "g.E().count()")); + return graphCount; + } + + private String queryCount(HugeClient client, String gremlin) { + try { + ResultSet result = this.queryService.executeQueryCount(client, gremlin); + if (result.data() == null || result.data().isEmpty()) { + return "max"; + } + return String.valueOf(result.data().get(0)); + } catch (Throwable e) { + LOG.warn("Failed to execute gremlin overview count: {}", gremlin, e); + return "max"; + } + } + + /** + * 正常gremlin请求 && 图分析页面gremlin请求 + */ @PostMapping - public GremlinResult execute(@PathVariable("connId") int connId, + public GremlinResult gremlin(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody GremlinQuery query) { + return this.executeGremlin(graphSpace, graph, query); + } + + /* + * 用户对大模型生成的gremlin评价 + */ + @PostMapping("text2gremlin-report") + public Map reportText2Gremlin(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody Text2Gremlin text2Gremlin) { + // username graphspace graph text llm-gremlin exec-gremlin success nice + text2Gremlin.setText(text2Gremlin.getText().replaceAll(",", ",")); + LOG.info("{},{},{},{},{},{},{},{}", text2Gremlin.getUsername(), + graphSpace, graph, text2Gremlin.getText(), + text2Gremlin.getLlmGremlin(), text2Gremlin.getExecGremlin(), + text2Gremlin.getSuccess(), + text2Gremlin.getScore()); + return ImmutableMap.of("text2gremlin-report", "success"); + } + + private GremlinResult executeGremlin(String graphSpace, String graph, + GremlinQuery query) { this.checkParamsValid(query); Date createTime = HubbleUtil.nowDate(); // Insert execute history ExecuteStatus status = ExecuteStatus.RUNNING; ExecuteHistory history; - history = new ExecuteHistory(null, connId, 0L, ExecuteType.GREMLIN, - query.getContent(), status, AsyncTaskStatus.UNKNOWN, + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.GREMLIN, query.getContent(), + query.getText(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); this.historyService.save(history); StopWatch timer = StopWatch.createStarted(); try { - GremlinResult result = this.queryService.executeQuery(connId, query); + HugeClient client = this.authGremlinClient(graphSpace, graph); + GremlinResult result = + this.queryService.executeGremlinQuery(client, query); status = ExecuteStatus.SUCCESS; return result; } catch (Throwable e) { status = ExecuteStatus.FAILED; + history.setFailureReason(GREMLIN_EXECUTION_FAILED); throw e; } finally { timer.stop(); @@ -95,27 +162,39 @@ public GremlinResult execute(@PathVariable("connId") int connId, } @PostMapping("async-task") - public ExecuteStatus executeAsyncTask(@PathVariable("connId") int connId, - @RequestBody GremlinQuery query) { + public Map executeAsyncTask( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody GremlinQuery query) { this.checkParamsValid(query); Date createTime = HubbleUtil.nowDate(); // Insert execute history ExecuteStatus status = ExecuteStatus.ASYNC_TASK_RUNNING; ExecuteHistory history; - history = new ExecuteHistory(null, connId, 0L, ExecuteType.GREMLIN_ASYNC, - query.getContent(), status, - AsyncTaskStatus.UNKNOWN, -1L, createTime); + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.GREMLIN_ASYNC, + query.getContent(), query.getText(), + status, AsyncTaskStatus.UNKNOWN, -1L, + createTime); this.historyService.save(history); StopWatch timer = StopWatch.createStarted(); long asyncId = 0L; + Map result = new HashMap<>(3); try { - asyncId = this.queryService.executeAsyncTask(connId, query); + HugeClient client = this.authGremlinClient(graphSpace, graph); + asyncId = this.queryService.executeGremlinAsyncTask(client, query); status = ExecuteStatus.ASYNC_TASK_SUCCESS; - return status; + result.put("task_id", asyncId); + result.put("execute_status", status); + return result; } catch (Throwable e) { status = ExecuteStatus.ASYNC_TASK_FAILED; + // TODO: Persist an async failure reason only after the Server Task + // DTO exposes a stable, sanitized reason code. Depending on task + // status alone cannot distinguish submission from execution + // failures; remove this TODO when that Server capability exists. throw e; } finally { timer.stop(); @@ -128,11 +207,13 @@ public ExecuteStatus executeAsyncTask(@PathVariable("connId") int connId, } @PutMapping - public GremlinResult expand(@PathVariable("connId") int connId, + public GremlinResult expand(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody AdjacentQuery query) { this.checkParamsValid(query); try { - return this.queryService.expandVertex(connId, query); + HugeClient client = this.authGremlinClient(graphSpace, graph); + return this.queryService.expandVertex(client, query); } catch (Exception e) { Throwable rootCause = Ex.rootCause(e); throw new ExternalException("gremlin.expand.failed", rootCause, @@ -160,12 +241,41 @@ private void checkParamsValid(AdjacentQuery query) { Ex.check(!StringUtils.isEmpty(condition.getOperator()), "common.param.cannot-be-null-or-empty", "condition.operator"); - Ex.check(CONDITION_OPERATORS.contains(condition.getOperator()), + Ex.check(QueryService.CONDITION_OPERATORS.contains( + condition.getOperator()), "common.param.should-belong-to", "condition.operator", - CONDITION_OPERATORS); + QueryService.CONDITION_OPERATORS); Ex.check(condition.getValue() != null, "common.param.cannot-be-null", "condition.value"); } } } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + private static class Text2Gremlin { + @JsonProperty("username") + private String username; + + // 执行的gremlin语句 + @JsonProperty("exec-gremlin") + private String execGremlin; + + @JsonProperty("text") + private String text; + + // 大模型生成的gremlin语句 + @JsonProperty("llm-gremlin") + private String llmGremlin; + + // 语句成功与否 + @JsonProperty("success") + private Boolean success; + + // 用户点赞与否 + @JsonProperty("score") + private Boolean score; + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/GraphMetricsController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/GraphMetricsController.java new file mode 100644 index 000000000..9ec1a6a92 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/GraphMetricsController.java @@ -0,0 +1,480 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.saas; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hugegraph.api.graph.GraphMetricsAPI; +// TODO fix import +//import org.apache.hugegraph.client.api.graph.GraphMetricsAPI; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.date.SafeDateFormat; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.util.DateUtil; +import org.apache.hugegraph.util.E; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequestMapping(Constant.API_VERSION + "/graphspaces/{graphspace}/graphs/{graph}/metrics") +public class GraphMetricsController extends BaseController { + private static final Logger LOG = LoggerFactory.getLogger( + GraphMetricsController.class); + + public static final SafeDateFormat SAFE_DATE_FORMAT = new SafeDateFormat("yyyyMMdd"); + + @GetMapping("schema") + public Object schemaMetrics(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph) { + + HugeClient client = this.authClient(graphSpace, graph); + String today = SAFE_DATE_FORMAT.format(DateUtil.now()); + + // 获取当前日期前14天的记录 + List dateRange = getRangeDate(today, -13); + + LOG.info("schema metrics info: query type count from = [{}], to = [{}]", + dateRange.get(0), dateRange.get(13)); + + SchemaCount schemaCount = + new SchemaCount<>(client.schema().getPropertyKeys().size(), + client.schema().getVertexLabels().size(), + client.schema().getEdgeLabels().size(), + client.schema().getIndexLabels().size()); + + GraphMetricsAPI.TypeCounts rangeTypeCount = + client.graph().getTypeCounts(dateRange.get(0), + dateRange.get(dateRange.size() - 1)); + + SchemaCount schemaWeekRate = + weeklyGrowthRate(rangeTypeCount.getTypeCounts(), dateRange, schemaCount); + + return new SchemaMetrics(schemaWeekRate, schemaCount); + } + + /** + * @return 返回当前日期前14天的日期列表 + */ + @GetMapping("element") + public Object elementMetrics( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "from", required = true) + @DateTimeFormat(pattern = "yyyyMMdd") String from, + @RequestParam(name = "to", required = true) + @DateTimeFormat(pattern = "yyyyMMdd") String to) { + HugeClient client = this.authClient(graphSpace, graph); + return getEvCount(client, graphSpace, graph, from, to); + } + + /** + * 统计单种类型顶点或者边的日增长率及其存量数据增长率 + * 及其他统计信息 + * @param from 起始日期 + * @param to 终止日期 + * @param type 顶点或边 + * @param schema schema名称 + * @return + */ + @GetMapping("element/schema") + public Object elementSchemaMetrics( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "from", required = true) + @DateTimeFormat(pattern = "yyyyMMdd") String from, + @RequestParam(name = "to", required = true) + @DateTimeFormat(pattern = "yyyyMMdd") String to, + @RequestParam(name = "type", required = true) String type, + @RequestParam(name = "schema", required = true) String schema) { + // 校验type参数 + E.checkArgument("vertex".equals(type) || "edge".equals(type), + String.format("type must in [vertex, edge], but got '%s'", type)); + + // 校验schema存不存在 + HugeClient client = this.authClient(graphSpace, graph); + boolean schemaExisted = false; + if ("vertex".equals(type)) { + schemaExisted = client.schema().getVertexLabel(schema) != null; + } else { + schemaExisted = client.schema().getEdgeLabel(schema) != null; + } + E.checkArgument(schemaExisted, + String.format("schema '%s' not existed", schema)); + + // 计算单种类型顶点或者边的日增长率及其存量数据增长率 + PerEvEntity result = calGrowthRates(client, type, schema); + + // 组织当前schema顶点或者边按日期分布的数据 + ArrayList itemCounts = new ArrayList<>(); + GraphMetricsAPI.TypeCounts typeCounts = + client.graph().getTypeCounts(from, to); + for (Map.Entry entry : + typeCounts.getTypeCounts().entrySet()) { + String k = entry.getKey(); + GraphMetricsAPI.TypeCount v = entry.getValue(); + if (v == null) { + itemCounts.add(new ItemCount(k, null, 0L)); + } else { + itemCounts.add(new ItemCount(k, v.getDatetime(), + getEleCount(type, schema, v))); + } + } + result.setCountBySchema(itemCounts); + + return result; + } + + private PerEvEntity calGrowthRates(HugeClient client, String type, + String schema) { + String today = SAFE_DATE_FORMAT.format(DateUtil.now()); + List last4Days = getRangeDate(today, -3); + GraphMetricsAPI.TypeCounts last3TypeCount = + client.graph().getTypeCounts(last4Days.get(0), last4Days.get(2)); + + Long[] last3Days = extractTotalCountLast3Days(last3TypeCount, type, schema, + last4Days.subList(0, 3)); + Long totalCount1 = last3Days[2]; // 前1天当前schema总数 + Long totalCount2 = last3Days[1]; // 前2天当前schema总数 + Long totalCount3 = last3Days[0]; // 前3天当前schema总数 + + // 设置总数值及增长量值 + return PerEvEntity.builder() + .addedGrowthRate(growthRate( + totalCount2 - totalCount3, + totalCount1 - totalCount2)) + .totalGrowthRate(growthRate(totalCount2, totalCount1)) + .preDayAdded(totalCount1 - totalCount2) + .preDayTotal(totalCount1) + .type(schema).build(); + } + + public static List getEvCount(HugeClient client, String gs, + String graph, String from, + String to) { + client.assignGraph(gs, graph); + GraphMetricsAPI.TypeCounts typeCounts = + client.graph().getTypeCounts(from, to); + + List list = new ArrayList<>(); + for (Map.Entry entry : + typeCounts.getTypeCounts().entrySet()) { + list.add(new EvCountEntity(entry.getKey(), entry.getValue())); + } + return list; + + } + + private Long[] extractTotalCountLast3Days( + GraphMetricsAPI.TypeCounts typeCounts, String type, + String schema, List dateRange) { + + if (typeCounts.getTypeCounts() == null) { + return new Long[]{0L, 0L, 0L}; + } + + ArrayList list = new ArrayList<>(); + for (int i = 0; i < dateRange.size(); i++) { + GraphMetricsAPI.TypeCount typeCount = + typeCounts.getTypeCounts().get(dateRange.get(i)); + Long totalCount = getEleCount(type, schema, typeCount); + list.add(totalCount); + } + + E.checkState(list.size() == 3, + "The 3-day count list size must be 3, but got %s", + list.size()); + return list.toArray(new Long[3]); + } + + private Long getEleCount(String type, String schema, + GraphMetricsAPI.TypeCount typeCount) { + if (typeCount == null) { + return 0L; + } + + Map countMap; + if ("vertex".equals(type)) { + countMap = typeCount.getVertices(); + } else if ("edge".equals(type)) { + countMap = typeCount.getEdges(); + } else { + throw new HugeException("undefine type: type must in [vertex, edge]"); + } + + if (countMap == null || countMap.get(schema) == null) { + return 0L; + } + + return countMap.get(schema); + } + + /** + * 计算周增长率 + * + * @param map 日期范围下类型统计数据 + * @param dateRange 日期范围 + * @param todaySchema 当前日期下的schema统计 + * @return schema的周增长率 + */ + public static SchemaCount weeklyGrowthRate( + Map map, + List dateRange, + SchemaCount todaySchema) { + + E.checkArgument(dateRange.size() == 14, + "The weekly growth date range size must be 14"); + if (map == null) { + map = Collections.emptyMap(); + } + + SchemaCount s1 = new SchemaCount<>(0L, 0L, 0L, 0L); + SchemaCount s2 = new SchemaCount<>(0L, 0L, 0L, 0L); + // 统计近一周的前一周 + for (int i = 0; i < 7; i++) { + GraphMetricsAPI.TypeCount typeCount = map.get(dateRange.get(i)); + if (typeCount == null) { + continue; + } + s1.pk = s1.pk + typeCount.getSchemas().get("pk_count"); + s1.vl = s1.vl + typeCount.getSchemas().get("vl_count"); + s1.el = s1.el + typeCount.getSchemas().get("el_count"); + s1.il = s1.il + typeCount.getSchemas().get("il_count"); + } + + // 近一周:取最近6天的数据 + 当天数据用实时数据 + s2.pk = s2.pk + todaySchema.pk; + s2.vl = s2.vl + todaySchema.vl; + s2.el = s2.el + todaySchema.el; + s2.il = s2.il + todaySchema.il; + for (int i = 7; i < 13; i++) { + GraphMetricsAPI.TypeCount typeCount = map.get(dateRange.get(i)); + if (typeCount == null) { + continue; + } + s2.pk = s2.pk + typeCount.getSchemas().get("pk_count"); + s2.vl = s2.vl + typeCount.getSchemas().get("vl_count"); + s2.el = s2.el + typeCount.getSchemas().get("el_count"); + s2.il = s2.il + typeCount.getSchemas().get("il_count"); + } + return new SchemaCount<>(growthRate(s1.pk, s2.pk), + growthRate(s1.vl, s2.vl), + growthRate(s1.el, s2.el), + growthRate(s1.il, s2.il)); + } + + /** + * @return num2 - num1 相比于 num1 的增长率 + * 只保留两位小数 + */ + private static Double growthRate(Long num1, Long num2) { + E.checkArgument(num1 != null, "num1 can't be null"); + E.checkArgument(num2 != null, "num2 can't be null"); + E.checkArgument(num1 >= 0, "num1 must be non-negative"); + E.checkArgument(num2 >= 0, "num2 must be non-negative"); + if (num1 == 0 && num2 == 0) { + return 0.0; + } + if (num1 == 0) { + return 1.0; + } + if (num2 == 0) { + return -1.0; + } + // 保留两位小数 + DecimalFormat decimalFormat = new DecimalFormat("#.##"); + String formattedResult = + decimalFormat.format((num2 - num1) / ((double) num1)); + return Double.parseDouble(formattedResult); + } + + public static List getRangeDate(String start, int days) { + List dateList = new ArrayList<>(); + try { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(SAFE_DATE_FORMAT.parse(start)); + + for (int i = 0; i <= Math.abs(days); i++) { + dateList.add(SAFE_DATE_FORMAT.format(calendar.getTime())); + if (days > 0) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } else { + calendar.add(Calendar.DAY_OF_MONTH, -1); + } + } + + } catch (Exception e) { + LOG.error("Failed to parse graph metrics date range", e); + } + return dateList.stream().sorted().collect(Collectors.toList()); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class PerEvEntity { + @JsonProperty("total_growth_rate") + public Double totalGrowthRate = 0.0; + @JsonProperty("added_growth_rate") + public Double addedGrowthRate = 0.0; + + @JsonProperty("pre_day_total") + public Long preDayTotal = 0L; + @JsonProperty("pre_day_added") + public Long preDayAdded = 0L; + @JsonProperty("type") + public String type; + @JsonProperty("count_by_schema") + public List countBySchema = new ArrayList<>(); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class EvCountEntity { + @JsonProperty("vertices_count") + public Long verticesCount = 0L; + + @JsonProperty("edges_count") + public Long edgesCount = 0L; + + @JsonProperty("date") + public String date; + + @JsonProperty("update_time") + public String updateTime; + + @JsonProperty("vertices") + public List vertices = new ArrayList<>(); + @JsonProperty("edges") + public List edges = new ArrayList<>(); + + public EvCountEntity(String date, GraphMetricsAPI.TypeCount typeCount) { + this.date = date; + if (typeCount == null) { + return; + } + this.vertices = mergeItem(typeCount.getVertices()); + this.edges = mergeItem(typeCount.getEdges()); + this.updateTime = typeCount.getDatetime(); + this.verticesCount = mergeCount(typeCount.getVertices()); + this.edgesCount = mergeCount(typeCount.getEdges()); + } + + private List mergeItem(Map evTypeCount) { + ArrayList list = new ArrayList<>(); + if (evTypeCount == null) { + return list; + } + for (Map.Entry entry : evTypeCount.entrySet()) { + Item item = new Item(entry.getKey(), entry.getValue()); + list.add(item); + } + return list; + } + + private Long mergeCount(Map map) { + Long count = 0L; + for (Map.Entry entry : map.entrySet()) { + count += entry.getValue(); + } + return count; + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Item { + @JsonProperty("type") + private String type; + + @JsonProperty("count") + private Long count; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ItemCount { + @JsonProperty("date") + private String date; + + @JsonProperty("update_time") + private String updateTime; + + @JsonProperty("count") + private Long count; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class SchemaMetrics { + @JsonProperty("weekly_growth_rate") + private SchemaCount weeklyGrowthRate; + + @JsonProperty("item_count") + private SchemaCount itemCount; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class SchemaCount { + @JsonProperty("pk") + public T pk; + + @JsonProperty("vl") + public T vl; + + @JsonProperty("el") + public T el; + + @JsonProperty("il") + public T il; + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/GraphSpaceMetricsController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/GraphSpaceMetricsController.java new file mode 100644 index 000000000..862152558 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/GraphSpaceMetricsController.java @@ -0,0 +1,147 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.saas; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.hugegraph.util.DateUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.api.graph.GraphMetricsAPI; +// TODO fix import +//import org.apache.hugegraph.client.api.graph.GraphMetricsAPI; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.service.graphs.GraphsService; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequestMapping(Constant.API_VERSION + "/graphspaces/{graphspace}/metrics") +public class GraphSpaceMetricsController extends BaseController { + @Autowired + private GraphsService g; + + @GetMapping("element") + public Object elementMetrics( + @PathVariable("graphspace") String graphSpace, + @DateTimeFormat(pattern = "yyyyMMdd") String from, + @RequestParam(name = "to", required = true) + @DateTimeFormat(pattern = "yyyyMMdd") String to) { + HugeClient client = this.authClient(graphSpace, null); + Set graphs = g.listGraphNames(client, graphSpace, ""); + + List result = new ArrayList<>(); + + Map gsTypeCounts = new HashMap<>(); + + for (String graph : graphs) { + client.assignGraph(graphSpace, graph); + GraphMetricsAPI.TypeCounts typeCounts = + client.graph().getTypeCounts(from, to); + mergeEvCount(gsTypeCounts, typeCounts); + } + + List list = new ArrayList<>(); + for (Map.Entry entry : + gsTypeCounts.entrySet()) { + list.add(new GraphMetricsController.EvCountEntity(entry.getKey(), + entry.getValue())); + } + return list; + } + + @GetMapping("schema") + public Object schemaMetrics( + @PathVariable("graphspace") String graphSpace) { + HugeClient client = this.authClient(graphSpace, null); + Set graphs = g.listGraphNames(client, graphSpace, ""); + + String today = GraphMetricsController.SAFE_DATE_FORMAT.format(DateUtil.now()); + + // 获取当前日期前14天的记录 + List dateRange = GraphMetricsController.getRangeDate(today, -13); + + Map gsTypeCounts = new HashMap<>(); + + GraphMetricsController.SchemaCount schemaCount = + new GraphMetricsController.SchemaCount<>(0, 0, 0, 0); + for (String graph : graphs) { + client.assignGraph(graphSpace, graph); + GraphMetricsAPI.TypeCounts typeCounts = + client.graph().getTypeCounts(dateRange.get(0), dateRange.get(13)); + mergeEvCount(gsTypeCounts, typeCounts); + schemaCount.pk += client.schema().getPropertyKeys().size(); + schemaCount.vl += client.schema().getVertexLabels().size(); + schemaCount.el += client.schema().getEdgeLabels().size(); + schemaCount.il += client.schema().getIndexLabels().size(); + } + + GraphMetricsController.SchemaCount schemaWeekRate = + GraphMetricsController.weeklyGrowthRate(gsTypeCounts, dateRange, schemaCount); + + return new GraphMetricsController.SchemaMetrics(schemaWeekRate, + schemaCount); + } + + private void mergeEvCount( + Map merged, + GraphMetricsAPI.TypeCounts evCount) { + for (Map.Entry entry : + evCount.getTypeCounts().entrySet()) { + String k = entry.getKey(); + GraphMetricsAPI.TypeCount v = entry.getValue(); + GraphMetricsAPI.TypeCount gsTypeCount = merged.get(k); + if (gsTypeCount == null) { + merged.put(k, v); + } else { + // 部分图没有数据,continue + if (v == null) { + continue; + } + + String updateTime = + gsTypeCount.getDatetime().compareTo(v.getDatetime()) > + 0 ? gsTypeCount.getDatetime() : v.getDatetime(); + gsTypeCount.setDatetime(updateTime); + mergeEvCount(gsTypeCount.getVertices(), v.getVertices()); + mergeEvCount(gsTypeCount.getEdges(), v.getEdges()); + } + } + } + + private void mergeEvCount(Map merged, Map map) { + for (Map.Entry entry : map.entrySet()) { + merged.merge(entry.getKey(), entry.getValue(), Long::sum); + } + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java new file mode 100644 index 000000000..cd23903a4 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java @@ -0,0 +1,347 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.saas; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.common.AppType; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.ApplicationInfo; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.query.ApplicationInfoService; +import org.apache.hugegraph.service.saas.PrometheusService; +import org.apache.hugegraph.service.space.GraphSpaceService; +import org.apache.hugegraph.util.HubbleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@RestController +@RequestMapping(Constant.API_VERSION + "saas/") +public class SaaSMetricsController extends BaseController { + + Logger logger = LoggerFactory.getLogger(SaaSMetricsController.class); + @Autowired + private GraphSpaceService graphSpaceService; + @Autowired + private PrometheusService prometheusService; + @Autowired + private HugeConfig config; + @Autowired + private ApplicationInfoService appInfoService; + + private Cache saasMetricsCache = + CacheBuilder.newBuilder() + .expireAfterWrite(4 * 3600, TimeUnit.SECONDS) + .build(); + + private static final String BASE_COUNT_QUERY = + "sum(nginx_vts_filter_requests_total{__CONDITIONS__})"; + private static final String BASE_DIS_QUERY = + "sum(rate(nginx_vts_filter_requests_total{__CONDITIONS__}[2m]))"; + + @GetMapping("graphspaces/{graphspace}/graphs/{graph}/request-details") + public Object appsRequestDetails(@PathVariable String graphspace, + @PathVariable String graph) { + String idc = config.get(HubbleOptions.IDC); + String graphName = join(idc, graphspace, graph); + List appInfos = appInfoService.query(graphName); + // 添加3个以下默认应用 GREMLIN, VERTEX_UPDATE, EDGE_UPDATE + appInfos.add(defaultGremlinApp(idc, graphspace, graph)); + appInfos.add(defaultVertexUpdateApp(idc, graphspace, graph)); + appInfos.add(defaultEdgeUpdateApp(idc, graphspace, graph)); + + long[] timestamps = HubbleUtil.getTimestampsBefore24Hours(); + long from = timestamps[0]; + long to = timestamps[1]; + List appsRequestDetails = new ArrayList<>(); + for (ApplicationInfo appInfo : appInfos) { + appsRequestDetails.add(getAppRequestDetails(appInfo, from, to)); + } + + ApplicationInfo graphAppInfo = defaultGraphApp(idc, graphspace); + AppRequestDetails totalGraph = + getAppRequestDetails(graphAppInfo, from, to); + + return RequestDetails.builder() + .appNum(appInfos.size()) + .appsRequestDetails(appsRequestDetails) + .countDuring24h(totalGraph.getCountDuring24h()) + .distributionDuring24h( + totalGraph.getDistributionDuring24h()) + .distributionQuery( + graphAppInfo.getDistributionQuery()) + .countQuery(graphAppInfo.getCountQuery()) + .build(); + } + + @GetMapping("graphspaces/{graphspace}/graphs/{graph}/request-distribution") + public Object appRequestDetails(@PathVariable String graphspace, + @PathVariable String graph, + @RequestParam(name = "start", required = true) + long start, + @RequestParam(name = "end", required = true) + long end, + @RequestParam(name = "query", required = true) + String query, + @RequestParam(name = "step", required = true) + long step) { + return prometheusService.queryByRange(query, start, end, step); + } + + private AppRequestDetails getAppRequestDetails(ApplicationInfo appInfo, + long from, long to) { + long countDuring24h = + prometheusService.queryByDelta(appInfo.getCountQuery(), from, + to); + List> distributionDuring24h = + prometheusService.queryByRange(appInfo.getDistributionQuery(), + from, to, 900); + return AppRequestDetails.builder() + .appInfo(appInfo) + .countDuring24h(countDuring24h) + .distributionDuring24h(distributionDuring24h) + .build(); + } + + @GetMapping("metrics") + public Object metrics() { + HugeClient client = this.authClient(null, null); + try { + return this.saasMetricsCache.get("saas-metrics", () -> { + Map metrics = + this.graphSpaceService.metrics(client); + String lastDay = HubbleUtil.dateFormatLastDay(); + Long preDayCountTotal = + prometheusService.queryCountOffSet1Day(lastDay + " 23:59:59"); + SaaSMetrics saasMetrics = + SaaSMetrics.builder() + .edgeCount(metrics.get("eCount")) + .edgeLabelCount(metrics.get("elCount")) + .graphCount(metrics.get("gCount")) + .graphSpaceCount(metrics.get("gsCount")) + .vertexCount(metrics.get("vCount")) + .vertexLabelCount(metrics.get("vlCount")) + .preDayTaskCount(metrics.get("preDayTaskCount")) + .preDayQueryCount(preDayCountTotal) + .build(); + logger.info("saas metrics in [{}]: {}", + HubbleUtil.dateFormatLastDay(), saasMetrics); + this.saasMetricsCache.put("saas-metrics", saasMetrics); + return saasMetrics; + }); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + public static ApplicationInfo defaultGremlinApp(String idc, + String graphSpace, + String graph) { + // 默认的Gremlin应用 + String department = departmentName(graphSpace); + String baseCondition = + "idc=~\"__IDC__\", filter=~\".*__DEPARTMENT__.*\", " + + "filter_name=\"POST::/gremlin\""; + + String condition = baseCondition.replace("__DEPARTMENT__", department) + .replace("__IDC__", idc); + + ApplicationInfo.ApplicationInfoBuilder app = + ApplicationInfo.builder(); + app.appName("GREMLIN") + .appType(AppType.GENERAL) + .graphName(join(idc, graphSpace, graph)) + .countQuery(BASE_COUNT_QUERY.replace("__CONDITIONS__", condition)) + .distributionQuery( + BASE_DIS_QUERY.replace("__CONDITIONS__", condition)) + .description("Gremlin查询应用(通用应用)"); + return app.build(); + } + + public static ApplicationInfo defaultVertexUpdateApp(String idc, + String graphSpace, + String graph) { + // 默认的VertexUpdate应用 + String department = departmentName(graphSpace); + String baseCondition = + "idc=~\"__IDC__\", filter=~\".*__DEPARTMENT__.*\", " + + "filter_name=~\".*/graphspaces/__GRAPH_SPACE__/graphs/" + + "__GRAPH__/graph/vertices/batch\""; + + String condition = baseCondition.replace("__DEPARTMENT__", department) + .replace("__IDC__", idc) + .replace("__GRAPH_SPACE__", graphSpace) + .replace("__GRAPH__", graph); + + ApplicationInfo.ApplicationInfoBuilder app = + ApplicationInfo.builder(); + app.appName("VERTEX-UPDATE") + .graphName(join(idc, graphSpace, graph)) + .appType(AppType.GENERAL) + .countQuery(BASE_COUNT_QUERY.replace("__CONDITIONS__", condition)) + .distributionQuery(BASE_DIS_QUERY.replace("__CONDITIONS__", condition)) + .description("批量顶点更新应用(通用应用)"); + return app.build(); + } + + public static ApplicationInfo defaultEdgeUpdateApp(String idc, + String graphSpace, + String graph) { + // 默认的EdgeUpdate应用 + String department = departmentName(graphSpace); + String baseCondition = + "idc=~\"__IDC__\", filter=~\".*__DEPARTMENT__.*\", " + + "filter_name=~\".*/graphspaces/__GRAPH_SPACE__/graphs" + + "/__GRAPH__/graph/edges/batch\""; + + String condition = baseCondition.replace("__DEPARTMENT__", department) + .replace("__IDC__", idc) + .replace("__GRAPH_SPACE__", graphSpace) + .replace("__GRAPH__", graph); + + ApplicationInfo.ApplicationInfoBuilder app = + ApplicationInfo.builder(); + app.appName("EDGE-UPDATE") + .graphName(join(idc, graphSpace, graph)) + .appType(AppType.GENERAL) + .countQuery(BASE_COUNT_QUERY.replace("__CONDITIONS__", condition)) + .distributionQuery(BASE_DIS_QUERY.replace("__CONDITIONS__", condition)) + .description("批量边更新应用(通用应用)"); + return app.build(); + } + + private static ApplicationInfo defaultGraphApp(String idc, String graphSpace) { + // 默认的graph应用 + String department = departmentName(graphSpace); + String baseCondition = + "idc=~\"__IDC__\", filter=~\".*__DEPARTMENT__.*\""; + + String condition = baseCondition.replace("__DEPARTMENT__", department) + .replace("__IDC__", idc); + + ApplicationInfo.ApplicationInfoBuilder app = + ApplicationInfo.builder(); + app.appName("TOTAL-GRAPH") + .appType(AppType.GENERAL) + .countQuery(BASE_COUNT_QUERY.replace("__CONDITIONS__", condition)) + .distributionQuery(BASE_DIS_QUERY.replace("__CONDITIONS__", condition)); + return app.build(); + } + + private static String departmentName(String graphSpace) { + if (graphSpace.endsWith("gs")) { + return graphSpace.substring(0, graphSpace.length() - 2); + } else { + return graphSpace; + } + } + + private static String join(String idc, String graphSpace, String graph) { + return String.join("-", idc, graphSpace, graph); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + private static class SaaSMetrics { + @JsonProperty("vertex-label-count") + private long vertexLabelCount; + + @JsonProperty("vertex-count") + private long vertexCount; + + @JsonProperty("edge-label-count") + private long edgeLabelCount; + + @JsonProperty("edge-count") + private long edgeCount; + + @JsonProperty("graph-space-count") + private long graphSpaceCount; + + @JsonProperty("graph-count") + private long graphCount; + + @JsonProperty("pre-day-task-count") + private long preDayTaskCount; + + @JsonProperty("pre-day-query-count") + private long preDayQueryCount; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + private static class RequestDetails { + @JsonProperty("app_num") + private int appNum; + + @JsonProperty("count_during24h") + private long countDuring24h; + + @JsonProperty("distribution_during24h") + private List> distributionDuring24h; + + @JsonProperty("count_query") + private String countQuery; + + @JsonProperty("distribution_query") + private String distributionQuery; + + @JsonProperty("apps_request_details") + private List appsRequestDetails; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + private static class AppRequestDetails { + @JsonProperty("app_info") + private ApplicationInfo appInfo; + + @JsonProperty("count_during24h") + private long countDuring24h; + + @JsonProperty("distribution_during24h") + private List> distributionDuring24h; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaasGraphViewController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaasGraphViewController.java new file mode 100644 index 000000000..fd34bef72 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaasGraphViewController.java @@ -0,0 +1,172 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.saas; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.time.StopWatch; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.query.GremlinController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.enums.AsyncTaskStatus; +import org.apache.hugegraph.entity.enums.ExecuteStatus; +import org.apache.hugegraph.entity.enums.ExecuteType; +import org.apache.hugegraph.entity.query.ExecuteHistory; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.entity.query.GremlinResult; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.HubbleUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Log4j2 +@RestController +@RequestMapping(Constant.API_VERSION + + "/graphspaces/{graphspace}/graphs/{graph}/graphview") + +/** + * 此接口为定制Gremlin查询接口: + * 用途: 提供给前端查询Gremlin的接口,包装返回的数据用于前端画布展示 + */ +public class SaasGraphViewController extends GremlinController { + + @Autowired + private QueryService queryService; + @Autowired + private ExecuteHistoryService historyService; + + @PostMapping + public GremlinResult execute(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody GraphViewQuery query) { + this.checkParamsValid(query); + + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.GREMLIN, query.getGremlin(), + status, AsyncTaskStatus.UNKNOWN, + -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + + try { + HugeClient client = this.authClient(graphSpace, graph); + GremlinResult result = + this.queryService.executeGremlinQuery(client, + query.convert2GremlinQuery()); + status = ExecuteStatus.SUCCESS; + return result; + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + @PostMapping("async-task") + public Map executeAsyncTask( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody GraphViewQuery query) { + checkParamsValid(query); + + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.ASYNC_TASK_RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.GREMLIN_ASYNC, + query.getGremlin(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + + StopWatch timer = StopWatch.createStarted(); + long asyncId = 0L; + Map result = new HashMap<>(3); + + try { + HugeClient client = this.authClient(graphSpace, graph); + asyncId = this.queryService.executeGremlinAsyncTask(client, + query.convert2GremlinQuery()); + status = ExecuteStatus.ASYNC_TASK_SUCCESS; + result.put("task_id", asyncId); + result.put("execute_status", status); + return result; + } catch (Throwable e) { + status = ExecuteStatus.ASYNC_TASK_FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + history.setAsyncId(asyncId); + this.historyService.update(history); + } + } + + private void checkParamsValid(GraphViewQuery query) { + E.checkNotNull(query.getGremlin(), "params [gremlin] must not be null"); + checkContentLength(query.getGremlin()); + E.checkNotNull(query.getUserName(), "params [username] must not be null"); + E.checkNotNull(query.getGremlin(), "params [password] must not be null"); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class GraphViewQuery { + @JsonProperty("gremlin") + private String gremlin; + + @JsonProperty("username") + private String userName; + + @JsonProperty("password") + private String password; + + public GremlinQuery convert2GremlinQuery() { + return new GremlinQuery(this.gremlin); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/EdgeLabelController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/EdgeLabelController.java index b21a94ed8..b4de6355a 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/EdgeLabelController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/EdgeLabelController.java @@ -22,6 +22,20 @@ import java.util.List; import java.util.Set; +import org.apache.hugegraph.driver.HugeClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.entity.schema.ConflictCheckEntity; import org.apache.hugegraph.entity.schema.ConflictDetail; @@ -33,27 +47,16 @@ import org.apache.hugegraph.service.schema.PropertyIndexService; import org.apache.hugegraph.service.schema.PropertyKeyService; import org.apache.hugegraph.service.schema.VertexLabelService; +import org.apache.hugegraph.structure.constant.EdgeLabelType; import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.HubbleUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - import com.baomidou.mybatisplus.core.metadata.IPage; import com.google.common.collect.ImmutableList; @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/schema/edgelabels") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/schema/edgelabels") public class EdgeLabelController extends SchemaController { private static final List PRESET_COLORS = ImmutableList.of( @@ -75,12 +78,17 @@ public class EdgeLabelController extends SchemaController { private EdgeLabelService elService; @GetMapping("optional-colors") - public List getOptionalColors(@PathVariable("connId") int connId) { + public List getOptionalColors() { return PRESET_COLORS; } @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") + String graphSpace, + @PathVariable("graph") String graph, + @RequestParam(name = "edgelabel_type", + required = false) + String type, @RequestParam(name = "content", required = false) String content, @@ -95,30 +103,37 @@ public IPage list(@PathVariable("connId") int connId, required = false, defaultValue = "10") int pageSize) { - return this.listInPage(id -> this.elService.list(id), - connId, content, nameOrder, pageNo, pageSize); + HugeClient client = this.authClient(graphSpace, graph); + return this.listInPage(id -> this.elService.list(null, id, true, type), + client, content, nameOrder, pageNo, pageSize); } @GetMapping("{name}") - public EdgeLabelEntity get(@PathVariable("connId") int connId, + public EdgeLabelEntity get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("name") String name) { - return this.elService.get(name, connId); + HugeClient client = this.authClient(graphSpace, graph); + return this.elService.get(name, client); } @PostMapping - public void create(@PathVariable("connId") int connId, + public void create(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody EdgeLabelEntity entity) { - this.checkParamsValid(entity, connId, true); - this.checkEntityUnique(entity, connId, true); + HugeClient client = this.authClient(graphSpace, graph); + this.checkParamsValid(entity, client, true); + this.checkEntityUnique(entity, client, true); entity.setCreateTime(HubbleUtil.nowDate()); - this.elService.add(entity, connId); + this.elService.add(entity, client); } @PostMapping("check_conflict") public ConflictDetail checkConflict( - @PathVariable("connId") int connId, - @RequestParam("reused_conn_id") int reusedConnId, - @RequestBody ConflictCheckEntity entity) { + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam("reused_graphspace") String reusedGraphSpace, + @RequestParam("reused_graph") String reusedGraph, + @RequestBody ConflictCheckEntity entity) { Ex.check(!CollectionUtils.isEmpty(entity.getElEntities()), "common.param.cannot-be-empty", "edgelabels"); Ex.check(CollectionUtils.isEmpty(entity.getPkEntities()), @@ -127,7 +142,8 @@ public ConflictDetail checkConflict( "common.param.must-be-null", "propertyindexes"); Ex.check(CollectionUtils.isEmpty(entity.getVlEntities()), "common.param.must-be-null", "vertexlabels"); - Ex.check(connId != reusedConnId, "schema.conn.cannot-reuse-self"); + Ex.check(graphSpace != reusedGraphSpace && graph != reusedGraph, + "schema.conn.cannot-reuse-self"); Set pkNames = new HashSet<>(); Set piNames = new HashSet<>(); @@ -138,59 +154,70 @@ public ConflictDetail checkConflict( vlNames.addAll(e.getLinkLabels()); } List vlEntities; - vlEntities = this.vlService.list(vlNames, reusedConnId, false); + HugeClient client = this.authClient(graphSpace, graph); + HugeClient reusedClient = this.authClient(reusedGraphSpace, + reusedGraph); + vlEntities = this.vlService.list(vlNames, reusedClient, false); for (VertexLabelEntity e : vlEntities) { pkNames.addAll(e.getPropNames()); piNames.addAll(e.getIndexProps()); } - entity.setPkEntities(this.pkService.list(pkNames, reusedConnId, false)); - entity.setPiEntities(this.piService.list(piNames, reusedConnId, false)); + entity.setPkEntities(this.pkService.list(pkNames, reusedClient, false)); + entity.setPiEntities(this.piService.list(piNames, reusedClient, false)); entity.setVlEntities(vlEntities); - return this.elService.checkConflict(entity, connId, false); + return this.elService.checkConflict(entity, client, false); } @PostMapping("recheck_conflict") public ConflictDetail recheckConflict( - @PathVariable("connId") int connId, - @RequestBody ConflictCheckEntity entity) { + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ConflictCheckEntity entity) { Ex.check(!CollectionUtils.isEmpty(entity.getElEntities()), "common.param.cannot-be-empty", "edgelabels"); - return this.elService.checkConflict(entity, connId, true); + HugeClient client = this.authClient(graphSpace, graph); + return this.elService.checkConflict(entity, client, true); } @PostMapping("reuse") - public void reuse(@PathVariable("connId") int connId, + public void reuse(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody ConflictDetail detail) { - this.elService.reuse(detail, connId); + HugeClient client = this.authClient(graphSpace, graph); + this.elService.reuse(detail, client); } @PutMapping("{name}") - public void update(@PathVariable("connId") int connId, + public void update(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("name") String name, @RequestBody EdgeLabelUpdateEntity entity) { Ex.check(!StringUtils.isEmpty(name), "common.param.cannot-be-null-or-empty", name); entity.setName(name); + HugeClient client = this.authClient(graphSpace, graph); - this.elService.checkExist(name, connId); - checkParamsValid(this.pkService, entity, connId); - this.elService.update(entity, connId); + this.elService.checkExist(name, client); + checkParamsValid(this.pkService, entity, client); + this.elService.update(entity, client); } /** * Delete edge label doesn't need check checkUsing */ @DeleteMapping - public void delete(@PathVariable("connId") int connId, + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam("names") List names) { + HugeClient client = this.authClient(graphSpace, graph); for (String name : names) { - this.elService.checkExist(name, connId); - this.elService.remove(name, connId); + this.elService.checkExist(name, client); + this.elService.remove(name, client); } } - private void checkParamsValid(EdgeLabelEntity entity, int connId, + private void checkParamsValid(EdgeLabelEntity entity, HugeClient client, boolean checkCreateTime) { String name = entity.getName(); Ex.check(name != null, "common.param.cannot-be-null", "name"); @@ -198,19 +225,25 @@ private void checkParamsValid(EdgeLabelEntity entity, int connId, "schema.edgelabel.unmatch-regex"); Ex.check(checkCreateTime, () -> entity.getCreateTime() == null, "common.param.must-be-null", "create_time"); + + EdgeLabelType type = EdgeLabelType.valueOf(entity.getEdgeLabelType()); + // skip check for PARENT type + if (type.parent()) { + return; + } // Check source label and target label - checkRelation(entity, connId); + checkRelation(entity, client); // Check properties - checkProperties(this.pkService, entity.getProperties(), false, connId); + checkProperties(this.pkService, entity.getProperties(), false, client); // Check sort keys checkSortKeys(entity); // Check property index - checkPropertyIndexes(entity, connId); + checkPropertyIndexes(entity, client); // Check display fields and join symbols checkDisplayFields(entity); } - private void checkRelation(EdgeLabelEntity entity, int connId) { + private void checkRelation(EdgeLabelEntity entity, HugeClient client) { String sourceLabel = entity.getSourceLabel(); String targetLabel = entity.getTargetLabel(); Ex.check(!StringUtils.isEmpty(sourceLabel), @@ -220,8 +253,8 @@ private void checkRelation(EdgeLabelEntity entity, int connId) { "common.param.cannot-be-null-or-empty", "edgelabel.target_label"); - this.vlService.checkExist(sourceLabel, connId); - this.vlService.checkExist(targetLabel, connId); + this.vlService.checkExist(sourceLabel, client); + this.vlService.checkExist(targetLabel, client); } private void checkSortKeys(EdgeLabelEntity entity) { @@ -264,10 +297,10 @@ private static void checkDisplayFields(EdgeLabelEntity entity) { } } - private void checkEntityUnique(EdgeLabelEntity newEntity, int connId, + private void checkEntityUnique(EdgeLabelEntity newEntity, HugeClient client, boolean creating) { // The name must be unique String name = newEntity.getName(); - this.elService.checkNotExist(name, connId); + this.elService.checkNotExist(name, client); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyIndexController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyIndexController.java index 731b11f82..1cf36f4d0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyIndexController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyIndexController.java @@ -18,7 +18,9 @@ package org.apache.hugegraph.controller.schema; +import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.schema.PropertyIndex; import org.apache.hugegraph.service.schema.PropertyIndexService; import org.apache.hugegraph.structure.constant.HugeType; @@ -30,17 +32,17 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.baomidou.mybatisplus.core.metadata.IPage; - @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/schema/propertyindexes") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs/" + + "{graph}/schema/propertyindexes") public class PropertyIndexController extends SchemaController { @Autowired private PropertyIndexService service; @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam(name = "is_vertex_label") boolean isVertexLabel, @RequestParam(name = "content", @@ -55,11 +57,12 @@ public IPage list(@PathVariable("connId") int connId, defaultValue = "10") int pageSize) { HugeType type = isVertexLabel ? HugeType.VERTEX_LABEL : - HugeType.EDGE_LABEL; + HugeType.EDGE_LABEL; + HugeClient client = this.authClient(graphSpace, graph); if (StringUtils.isEmpty(content)) { - return this.service.list(connId, type, pageNo, pageSize); + return this.service.list(client, type, pageNo, pageSize); } else { - return this.service.list(connId, type, content, pageNo, pageSize); + return this.service.list(client, type, content, pageNo, pageSize); } } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyKeyController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyKeyController.java index cdb56acc3..b46982f5c 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyKeyController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/PropertyKeyController.java @@ -18,11 +18,10 @@ package org.apache.hugegraph.controller.schema; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.schema.ConflictCheckEntity; import org.apache.hugegraph.entity.schema.ConflictDetail; import org.apache.hugegraph.entity.schema.PropertyKeyEntity; @@ -42,20 +41,22 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.baomidou.mybatisplus.core.metadata.IPage; - -import lombok.extern.log4j.Log4j2; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/schema/propertykeys") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/schema/propertykeys") public class PropertyKeyController extends SchemaController { @Autowired private PropertyKeyService service; @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam(name = "content", required = false) String content, @@ -70,29 +71,35 @@ public IPage list(@PathVariable("connId") int connId, required = false, defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphSpace, graph); return this.listInPage(id -> this.service.list(id), - connId, content, nameOrder, pageNo, pageSize); + client, content, nameOrder, pageNo, pageSize); } @GetMapping("{name}") - public PropertyKeyEntity get(@PathVariable("connId") int connId, + public PropertyKeyEntity get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("name") String name) { - return this.service.get(name, connId); + HugeClient client = this.authClient(graphSpace, graph); + return this.service.get(name, client); } @PostMapping - public void create(@PathVariable("connId") int connId, + public void create(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody PropertyKeyEntity entity) { + HugeClient client = this.authClient(graphSpace, graph); this.checkParamsValid(entity, true); - this.checkEntityUnique(entity, connId); + this.checkEntityUnique(entity, client); entity.setCreateTime(HubbleUtil.nowDate()); - this.service.add(entity, connId); + this.service.add(entity, client); } @PostMapping("check_conflict") public ConflictDetail checkConflict( - @PathVariable("connId") int connId, - @RequestBody ConflictCheckEntity entity) { + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ConflictCheckEntity entity) { List entities = entity.getPkEntities(); Ex.check(!CollectionUtils.isEmpty(entities), "common.param.cannot-be-empty", "entities"); @@ -103,13 +110,15 @@ public ConflictDetail checkConflict( Ex.check(CollectionUtils.isEmpty(entity.getElEntities()), "common.param.must-be-null", "edgelabels"); entities.forEach(e -> this.checkParamsValid(e, false)); - return this.service.checkConflict(entity, connId, false); + HugeClient client = this.authClient(graphSpace, graph); + return this.service.checkConflict(entity, client, false); } @PostMapping("recheck_conflict") public ConflictDetail recheckConflict( - @PathVariable("connId") int connId, - @RequestBody ConflictCheckEntity entity) { + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ConflictCheckEntity entity) { Ex.check(!CollectionUtils.isEmpty(entity.getPkEntities()), "common.param.cannot-be-empty", "propertykeys"); Ex.check(CollectionUtils.isEmpty(entity.getPiEntities()), @@ -118,26 +127,31 @@ public ConflictDetail recheckConflict( "common.param.must-be-null", "vertexlabels"); Ex.check(CollectionUtils.isEmpty(entity.getElEntities()), "common.param.must-be-null", "edgelabels"); - return this.service.checkConflict(entity, connId, true); + HugeClient client = this.authClient(graphSpace, graph); + return this.service.checkConflict(entity, client, true); } @PostMapping("reuse") - public void reuse(@PathVariable("connId") int connId, + public void reuse(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody ConflictDetail detail) { Ex.check(!CollectionUtils.isEmpty(detail.getPkConflicts()), "common.param.cannot-be-empty", "propertykey_conflicts"); - this.service.reuse(detail, connId); + HugeClient client = this.authClient(graphSpace, graph); + this.service.reuse(detail, client); } @PostMapping("check_using") - public Map checkUsing(@PathVariable("connId") int connId, + public Map checkUsing(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody UsingCheckEntity entity) { Ex.check(!CollectionUtils.isEmpty(entity.getNames()), "common.param.cannot-be-empty", "names"); Map inUsing = new LinkedHashMap<>(); + HugeClient client = this.authClient(graphSpace, graph); for (String name : entity.getNames()) { - this.service.checkExist(name, connId); - inUsing.put(name, this.service.checkUsing(name, connId)); + this.service.checkExist(name, client); + inUsing.put(name, this.service.checkUsing(name, client)); } return inUsing; } @@ -146,14 +160,16 @@ public Map checkUsing(@PathVariable("connId") int connId, * Should request "check_using" before delete */ @DeleteMapping - public void delete(@PathVariable("connId") int connId, + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam List names, @RequestParam(name = "skip_using", defaultValue = "false") boolean skipUsing) { + HugeClient client = this.authClient(graphSpace, graph); for (String name : names) { - this.service.checkExist(name, connId); - if (this.service.checkUsing(name, connId)) { + this.service.checkExist(name, client); + if (this.service.checkUsing(name, client)) { if (skipUsing) { continue; } else { @@ -161,7 +177,7 @@ public void delete(@PathVariable("connId") int connId, name); } } - this.service.remove(name, connId); + this.service.remove(name, client); } } @@ -179,9 +195,10 @@ private void checkParamsValid(PropertyKeyEntity entity, "common.param.must-be-null", "create_time"); } - private void checkEntityUnique(PropertyKeyEntity newEntity, int connId) { + private void checkEntityUnique(PropertyKeyEntity newEntity, + HugeClient client) { // The name must be unique String name = newEntity.getName(); - this.service.checkNotExist(name, connId); + this.service.checkNotExist(name, client); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java index 9924afca5..58009368b 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java @@ -18,139 +18,189 @@ package org.apache.hugegraph.controller.schema; -import java.util.ArrayList; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Comparator; import java.util.Date; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.service.schema.SchemaService; +import org.apache.hugegraph.service.schema.GroovySchemaCompatibility; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; + +import org.apache.hugegraph.util.Log; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.controller.BaseController; -import org.apache.hugegraph.entity.schema.EdgeLabelEntity; import org.apache.hugegraph.entity.schema.LabelUpdateEntity; import org.apache.hugegraph.entity.schema.Property; import org.apache.hugegraph.entity.schema.PropertyIndex; -import org.apache.hugegraph.entity.schema.PropertyKeyEntity; import org.apache.hugegraph.entity.schema.SchemaEntity; import org.apache.hugegraph.entity.schema.SchemaLabelEntity; import org.apache.hugegraph.entity.schema.Timefiable; -import org.apache.hugegraph.entity.schema.VertexLabelEntity; import org.apache.hugegraph.exception.InternalException; -import org.apache.hugegraph.service.schema.EdgeLabelService; import org.apache.hugegraph.service.schema.PropertyKeyService; -import org.apache.hugegraph.service.schema.VertexLabelService; -import org.apache.hugegraph.structure.constant.IdStrategy; import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.PageUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import com.baomidou.mybatisplus.core.metadata.IPage; -import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/schema") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/schema") public class SchemaController extends BaseController { - @Autowired - private PropertyKeyService pkService; - @Autowired - private VertexLabelService vlService; - @Autowired - private EdgeLabelService elService; + public static Logger log = Log.logger(SchemaController.class); - @GetMapping("graphview") - public SchemaView displayInSchemaView(@PathVariable("connId") int connId) { - List propertyKeys = this.pkService.list(connId); - List vertexLabels = this.vlService.list(connId); - List edgeLabels = this.elService.list(connId); + @Autowired + private SchemaService schemaService; + @GetMapping("groovy") + public Object schemaGroovy(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph) { + HugeClient client = this.authClient(graphSpace, graph); + return ImmutableMap.of("schema", + GroovySchemaCompatibility.export(client.schema())); + } - List> vertices = new ArrayList<>(vertexLabels.size()); - for (VertexLabelEntity entity : vertexLabels) { - Map vertex = new LinkedHashMap<>(); - vertex.put("id", entity.getName()); - vertex.put("label", entity.getName()); - if (entity.getIdStrategy() == IdStrategy.PRIMARY_KEY) { - vertex.put("primary_keys", entity.getPrimaryKeys()); - } else { - vertex.put("primary_keys", new ArrayList<>()); - } - Map properties = new LinkedHashMap<>(); - this.fillProperties(properties, entity, propertyKeys); - vertex.put("properties", properties); - vertex.put("~style", entity.getStyle()); - vertices.add(vertex); + @PostMapping("groovy") + public Object addSchemaGroovy(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody SchemaGroovy schemaGroovy) { + HugeClient client = this.authClient(graphSpace, graph); + String content = schemaGroovy.getSchemaGroovy(); + log.info("Add schema groovy: {}", content); + checkSchemaGroovy(content); + try { + client.gremlin().gremlin(content).execute(); + } catch (Exception e) { + throw new HugeException( + "Add schema groovy failed. caused by" + e.getMessage()); } + return ImmutableMap.of("schema-groovy", content); + } - List> edges = new ArrayList<>(edgeLabels.size()); - for (EdgeLabelEntity entity : edgeLabels) { - Map edge = new LinkedHashMap<>(); - String edgeId = String.format( - "%s-%s->%s", entity.getSourceLabel(), - entity.getName(), entity.getTargetLabel()); - edge.put("id", edgeId); - edge.put("label", entity.getName()); - edge.put("source", entity.getSourceLabel()); - edge.put("target", entity.getTargetLabel()); - if (entity.isLinkMultiTimes()) { - edge.put("sort_keys", entity.getSortKeys()); - } else { - edge.put("sort_keys", new ArrayList<>()); + private void checkSchemaGroovy(String content) { + List statements; + try { + statements = GroovySchemaCompatibility.splitStatements(content); + } catch (IllegalArgumentException e) { + throw new ExternalException( + "Schema Groovy contains an unterminated string"); + } + for (String statement : statements) { + if (!statement.startsWith("graph.schema()")) { + throw new ExternalException( + "Schema Groovy each row must start with 'graph" + + ".schema().'"); } - Map properties = new LinkedHashMap<>(); - this.fillProperties(properties, entity, propertyKeys); - edge.put("properties", properties); - edge.put("~style", entity.getStyle()); - edges.add(edge); } - return new SchemaView(vertices, edges); } - private void fillProperties(Map properties, - SchemaLabelEntity entity, - List propertyKeys) { - for (Property property : entity.getProperties()) { - String name = property.getName(); - PropertyKeyEntity pkEntity = findPropertyKey(propertyKeys, name); - properties.put(name, pkEntity.getDataType().string()); + @GetMapping("groovy/export") + public void schemaGroovyExport(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + HttpServletResponse response) { + HugeClient client = this.authClient(graphSpace, graph); + String schema = GroovySchemaCompatibility.export(client.schema()); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", + contentDisposition(graphSpace, graph)); + try { + OutputStream os = response.getOutputStream(); + os.write(schema.getBytes(StandardCharsets.UTF_8)); + os.close(); + } catch (IOException e) { + throw new InternalException("Schema File Write Error", e); } } - private PropertyKeyEntity findPropertyKey(List entities, - String name) { - for (PropertyKeyEntity entity : entities) { - if (entity.getName().equals(name)) { - return entity; + static String contentDisposition(String graphSpace, String graph) { + String fileName = sanitizeFileName( + String.format("%s_%s.schema", graphSpace, graph)); + String fallback = asciiFileName(fileName); + return String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", + fallback, encodeFileName(fileName)); + } + + private static String sanitizeFileName(String fileName) { + StringBuilder builder = new StringBuilder(fileName.length()); + for (int i = 0; i < fileName.length(); i++) { + char c = fileName.charAt(i); + if (c == '\r' || c == '\n' || Character.isISOControl(c)) { + builder.append('_'); + } else { + builder.append(c); } } - throw new InternalException("schema.propertykey.not-exist", name); + String sanitized = builder.toString().trim(); + return sanitized.isEmpty() ? "schema.schema" : sanitized; } - @AllArgsConstructor - private static class SchemaView { + private static String asciiFileName(String fileName) { + StringBuilder builder = new StringBuilder(fileName.length()); + for (int i = 0; i < fileName.length(); i++) { + char c = fileName.charAt(i); + if (c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z' || + c >= '0' && c <= '9' || + c == '.' || c == '_' || c == '-') { + builder.append(c); + } else { + builder.append('_'); + } + } + return builder.toString(); + } - @JsonProperty("vertices") - private List> vertices; + private static String encodeFileName(String fileName) { + try { + return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()) + .replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 encoding is unavailable", e); + } + } - @JsonProperty("edges") - private List> edges; + @GetMapping("graphview") + public SchemaService.SchemaView displayInSchemaView( + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph) { + HugeClient client = this.authClient(graphSpace, graph); + return schemaService.getSchemaView(client); } public IPage listInPage( - Function> fetcher, - int connId, String content, - String nameOrder, - int pageNo, int pageSize) { + Function> fetcher, + HugeClient client, String content, + String nameOrder, + int pageNo, int pageSize) { Boolean nameOrderAsc = null; if (!StringUtils.isEmpty(nameOrder)) { Ex.check(ORDER_ASC.equals(nameOrder) || ORDER_DESC.equals(nameOrder), @@ -158,7 +208,7 @@ public IPage listInPage( nameOrderAsc = ORDER_ASC.equals(nameOrder); } - List entities = fetcher.apply(connId); + List entities = fetcher.apply(client); if (!StringUtils.isEmpty(content)) { // Select by content entities = entities.stream() @@ -197,8 +247,10 @@ public void sortByName(List entities, public void sortByCreateTime(List entities, boolean asc) { Comparator dateAscComparator = (o1, o2) -> { - assert o1 instanceof Timefiable; - assert o2 instanceof Timefiable; + if (!(o1 instanceof Timefiable) || !(o2 instanceof Timefiable)) { + throw new InternalException("Schema entities must implement " + + "Timefiable for create time sort"); + } Date t1 = ((Timefiable) o1).getCreateTime(); Date t2 = ((Timefiable) o2).getCreateTime(); if (t1 == null && t2 == null) { @@ -238,20 +290,21 @@ public void sortByRelativity(List entities, */ public static void checkProperties(PropertyKeyService service, Set properties, - boolean mustNullable, int connId) { + boolean mustNullable, + HugeClient client) { if (properties == null) { return; } for (Property property : properties) { String pkName = property.getName(); - service.checkExist(pkName, connId); + service.checkExist(pkName, client); Ex.check(mustNullable, property::isNullable, "schema.propertykey.must-be-nullable", pkName); } } public static void checkPropertyIndexes(SchemaLabelEntity entity, - int connId) { + HugeClient client) { List propertyIndexes = entity.getPropertyIndexes(); if (propertyIndexes != null) { for (PropertyIndex propertyIndex : propertyIndexes) { @@ -268,8 +321,23 @@ public static void checkPropertyIndexes(SchemaLabelEntity entity, } public static void checkParamsValid(PropertyKeyService service, - LabelUpdateEntity entity, int connId) { + LabelUpdateEntity entity, + HugeClient client) { // All append property should be nullable - checkProperties(service, entity.getAppendProperties(), true, connId); + checkProperties(service, entity.getAppendProperties(), true, client); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class SchemaGroovy { + @JsonProperty("schema-groovy") + private String schemaGroovy; + + @Override + public String toString() { + return this.schemaGroovy; + } } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/VertexLabelController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/VertexLabelController.java index b34608d56..6a8beea4a 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/VertexLabelController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/VertexLabelController.java @@ -18,11 +18,29 @@ package org.apache.hugegraph.controller.schema; -import java.util.HashSet; -import java.util.LinkedHashMap; +//import java.util.*; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.stream.Collectors; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.schema.vertexlabel.ParamEntity; +import org.apache.hugegraph.entity.schema.vertexlabel.ParamStyle; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.entity.schema.ConflictCheckEntity; @@ -39,24 +57,12 @@ import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.HubbleUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - import com.baomidou.mybatisplus.core.metadata.IPage; import com.google.common.collect.ImmutableList; @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/schema/vertexlabels") +@RequestMapping(Constant.API_V1_3 + "graphspaces/{graphspace}/graphs" + + "/{graph}/schema/vertexlabels") public class VertexLabelController extends SchemaController { private static final List PRESET_COLORS = ImmutableList.of( @@ -76,19 +82,22 @@ public class VertexLabelController extends SchemaController { private VertexLabelService vlService; @GetMapping("optional-colors") - public List getOptionalColors(@PathVariable("connId") int connId) { + public List getOptionalColors() { return PRESET_COLORS; } @GetMapping("{name}/link") - public List getLinkEdgeLabels(@PathVariable("connId") int connId, + public List getLinkEdgeLabels(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("name") String name) { - this.vlService.checkExist(name, connId); - return this.vlService.getLinkEdgeLabels(name, connId); + HugeClient client = this.authClient(graphSpace, graph); + this.vlService.checkExist(name, client); + return this.vlService.getLinkEdgeLabels(name, client); } @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam(name = "content", required = false) String content, @@ -103,30 +112,38 @@ public IPage list(@PathVariable("connId") int connId, required = false, defaultValue = "10") int pageSize) { - return this.listInPage(id -> this.vlService.list(id), - connId, content, nameOrder, pageNo, pageSize); + HugeClient client = this.authClient(graphSpace, graph); + return this.listInPage(c -> this.vlService.list(c), + client, content, nameOrder, pageNo, pageSize); } @GetMapping("{name}") - public VertexLabelEntity get(@PathVariable("connId") int connId, + public VertexLabelEntity get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("name") String name) { - return this.vlService.get(name, connId); + HugeClient client = this.authClient(graphSpace, graph); + return this.vlService.get(name, client); } @PostMapping - public void create(@PathVariable("connId") int connId, + public void create(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody VertexLabelEntity entity) { - this.checkParamsValid(entity, connId, true); - this.checkEntityUnique(entity, connId, true); + HugeClient client = this.authClient(graphSpace, graph); + + this.checkParamsValid(entity, client, true); + this.checkEntityUnique(entity, client, true); entity.setCreateTime(HubbleUtil.nowDate()); - this.vlService.add(entity, connId); + this.vlService.add(entity, client); } @PostMapping("check_conflict") public ConflictDetail checkConflicts( - @PathVariable("connId") int connId, - @RequestParam("reused_conn_id") int reusedConnId, - @RequestBody ConflictCheckEntity entity) { + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestParam("reused_graphspace") String reusedGraphSpace, + @RequestParam("reused_graph") String reusedGraph, + @RequestBody ConflictCheckEntity entity) { Ex.check(!CollectionUtils.isEmpty(entity.getVlEntities()), "common.param.cannot-be-empty", "vertexlabels"); Ex.check(CollectionUtils.isEmpty(entity.getPkEntities()), @@ -135,7 +152,8 @@ public ConflictDetail checkConflicts( "common.param.must-be-null", "propertyindexes"); Ex.check(CollectionUtils.isEmpty(entity.getElEntities()), "common.param.must-be-null", "edgelabels"); - Ex.check(connId != reusedConnId, "schema.conn.cannot-reuse-self"); + Ex.check(graphSpace != reusedGraphSpace && graph != reusedGraph, + "schema.conn.cannot-reuse-self"); Set pkNames = new HashSet<>(); Set piNames = new HashSet<>(); @@ -144,64 +162,78 @@ public ConflictDetail checkConflicts( piNames.addAll(e.getIndexProps()); } - entity.setPkEntities(this.pkService.list(pkNames, reusedConnId, false)); - entity.setPiEntities(this.piService.list(piNames, reusedConnId, false)); - return this.vlService.checkConflict(entity, connId, false); + HugeClient client = this.authClient(graphSpace, graph); + HugeClient reusedClient = this.authClient(reusedGraphSpace, reusedGraph); + + entity.setPkEntities(this.pkService.list(pkNames, reusedClient, false)); + entity.setPiEntities(this.piService.list(piNames, reusedClient, false)); + return this.vlService.checkConflict(entity, client, false); } @PostMapping("recheck_conflict") public ConflictDetail recheckConflicts( - @PathVariable("connId") int connId, - @RequestBody ConflictCheckEntity entity) { + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ConflictCheckEntity entity) { Ex.check(!CollectionUtils.isEmpty(entity.getVlEntities()), "common.param.cannot-be-empty", "vertexlabels"); Ex.check(CollectionUtils.isEmpty(entity.getElEntities()), "common.param.must-be-null", "edgelabels"); - return this.vlService.checkConflict(entity, connId, true); + + HugeClient client = this.authClient(graphSpace, graph); + return this.vlService.checkConflict(entity, client, true); } @PostMapping("reuse") - public void reuse(@PathVariable("connId") int connId, + public void reuse(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestBody ConflictDetail detail) { - this.vlService.reuse(detail, connId); + HugeClient client = this.authClient(graphSpace, graph); + this.vlService.reuse(detail, client); } @PutMapping("{name}") - public void update(@PathVariable("connId") int connId, + public void update(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("name") String name, @RequestBody VertexLabelUpdateEntity entity) { Ex.check(!StringUtils.isEmpty(name), "common.param.cannot-be-null-or-empty", name); entity.setName(name); + HugeClient client = this.authClient(graphSpace, graph); - this.vlService.checkExist(name, connId); - checkParamsValid(this.pkService, entity, connId); - this.vlService.update(entity, connId); + this.vlService.checkExist(name, client); + checkParamsValid(this.pkService, entity, client); + this.vlService.update(entity, client); } @PostMapping("check_using") public Map checkUsing( - @PathVariable("connId") int connId, - @RequestBody UsingCheckEntity entity) { + @PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody UsingCheckEntity entity) { Ex.check(!CollectionUtils.isEmpty(entity.getNames()), "common.param.cannot-be-empty", "names"); Map inUsing = new LinkedHashMap<>(); + HugeClient client = this.authClient(graphSpace, graph); for (String name : entity.getNames()) { - this.vlService.checkExist(name, connId); - inUsing.put(name, this.vlService.checkUsing(name, connId)); + this.vlService.checkExist(name, client); + inUsing.put(name, this.vlService.checkUsing(name, client)); } return inUsing; } @DeleteMapping - public void delete(@PathVariable("connId") int connId, + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam("names") List names, @RequestParam(name = "skip_using", defaultValue = "false") boolean skipUsing) { + HugeClient client = this.authClient(graphSpace, graph); for (String name : names) { - this.vlService.checkExist(name, connId); - if (this.vlService.checkUsing(name, connId)) { + this.vlService.checkExist(name, client); + if (this.vlService.checkUsing(name, client)) { if (skipUsing) { continue; } else { @@ -209,11 +241,11 @@ public void delete(@PathVariable("connId") int connId, name); } } - this.vlService.remove(name, connId); + this.vlService.remove(name, client); } } - private void checkParamsValid(VertexLabelEntity entity, int connId, + private void checkParamsValid(VertexLabelEntity entity, HugeClient client, boolean checkCreateTime) { String name = entity.getName(); Ex.check(name != null, "common.param.cannot-be-null", "name"); @@ -222,11 +254,11 @@ private void checkParamsValid(VertexLabelEntity entity, int connId, Ex.check(checkCreateTime, () -> entity.getCreateTime() == null, "common.param.must-be-null", "create_time"); // Check properties - checkProperties(this.pkService, entity.getProperties(), false, connId); + checkProperties(this.pkService, entity.getProperties(), false, client); // Check primary keys checkPrimaryKeys(entity); // Check property index - checkPropertyIndexes(entity, connId); + checkPropertyIndexes(entity, client); // Check display fields and join symbols checkDisplayFields(entity); } @@ -271,10 +303,98 @@ private void checkPrimaryKeys(VertexLabelEntity entity) { } } - private void checkEntityUnique(VertexLabelEntity newEntity, int connId, + private void checkEntityUnique(VertexLabelEntity newEntity, + HugeClient client, boolean creating) { // The name must be unique String name = newEntity.getName(); - this.vlService.checkNotExist(name, connId); + this.vlService.checkNotExist(name, client); + } + + @GetMapping("{name}/new") + public ParamEntity getNew(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("name") String name) { + HugeClient client = this.authClient(graphSpace, graph); + return this.labelDataToParam(this.vlService.get(name, client)); + } + + @PutMapping("{name}/new") + public void updateNew(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @PathVariable("name") String name, + @RequestBody VertexLabelUpdateEntity entity) { + Ex.check(!StringUtils.isEmpty(name), + "common.param.cannot-be-null-or-empty", name); + entity.setName(name); + HugeClient client = this.authClient(graphSpace, graph); + + this.vlService.checkExist(name, client); + checkParamsValid(this.pkService, entity, client); + this.vlService.update(entity, client); + } + + @PostMapping("create_new") + public void createNew(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, + @RequestBody ParamEntity entity) { + HugeClient client = this.authClient(graphSpace, graph); + + VertexLabelEntity formatEntity = this.paramToLabelData(entity); + + this.checkParamsValid(formatEntity, client, true); + this.checkEntityUnique(formatEntity, client, true); + formatEntity.setCreateTime(HubbleUtil.nowDate()); + this.vlService.add(formatEntity, client); + } + + private VertexLabelEntity paramToLabelData(ParamEntity entity) { + List displayFields = entity.getDisplayFields(); + + if (!CollectionUtils.isEmpty(displayFields)) { + displayFields.add("~id"); + } else { + displayFields = ImmutableList.of("~id"); + } + + VertexLabelStyle style = new VertexLabelStyle(); + style.setColor(entity.getStyle().getColor()); + style.setSize(entity.getStyle().getSize()); + style.setDisplayFields(displayFields); + + VertexLabelEntity newEntity = new VertexLabelEntity(); + newEntity.setIdStrategy(entity.getIdStrategy()); + newEntity.setName(entity.getName()); + newEntity.setProperties(entity.getProperties()); + newEntity.setPropertyIndexes(entity.getPropertyIndexes()); + newEntity.setOpenLabelIndex(entity.getOpenLabelIndex()); + newEntity.setPrimaryKeys(entity.getPrimaryKeys()); + newEntity.setStyle(style); + + return newEntity; + } + + // format backend data to front + private ParamEntity labelDataToParam(VertexLabelEntity entity) { + // Logger Logger = LoggerFactory.getLogger(getClass()); + + ParamStyle style = new ParamStyle(); + style.setColor(entity.getStyle().getColor()); + style.setSize(entity.getStyle().getSize()); + + List displayFields = entity.getStyle().getDisplayFields().stream() + .filter(v -> !("~id".equals(v))).collect(Collectors.toList()); + + ParamEntity newEntity = new ParamEntity(); + newEntity.setIdStrategy(entity.getIdStrategy()); + newEntity.setName(entity.getName()); + newEntity.setProperties(entity.getProperties()); + newEntity.setPropertyIndexes(entity.getPropertyIndexes()); + newEntity.setOpenLabelIndex(entity.getOpenLabelIndex()); + newEntity.setPrimaryKeys(entity.getPrimaryKeys()); + newEntity.setStyle(style); + newEntity.setDisplayFields(displayFields); + + return newEntity; } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/sketch/GraphSketchController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/sketch/GraphSketchController.java new file mode 100644 index 000000000..21dacab8a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/sketch/GraphSketchController.java @@ -0,0 +1,19 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.sketch; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/ComputerDisController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/ComputerDisController.java new file mode 100644 index 000000000..f74002e16 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/ComputerDisController.java @@ -0,0 +1,80 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.space; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.service.space.ComputerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/jobs/computerdis") +public class ComputerDisController extends BaseController { + + @Autowired + ComputerService computerService; + + @GetMapping + public IPage queryPage(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + HugeClient client = this.authClient(graphspace, graph); + return computerService.queryPage(client, query, pageNo, pageSize); + } + + @GetMapping("{id}/cancel") + public void cancel(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @PathVariable("id") long id) { + + HugeClient client = this.authClient(graphspace, graph); + computerService.cancel(client, id); + } + + @GetMapping("{id}") + public Object get(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @PathVariable("id") long id) { + HugeClient client = this.authClient(graphspace, graph); + return computerService.get(client, id); + } + + @DeleteMapping("{id}") + public void delete(@PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph, + @PathVariable("id") long id) { + HugeClient client = this.authClient(graphspace, graph); + computerService.delete(client, id); + + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/GraphSpaceController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/GraphSpaceController.java new file mode 100644 index 000000000..ecfb8ceb3 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/GraphSpaceController.java @@ -0,0 +1,240 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.space; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.GraphConnection; +import org.apache.hugegraph.entity.space.BuiltInEntity; +import org.apache.hugegraph.entity.space.GraphSpaceEntity; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.service.graphs.GraphsService; +import org.apache.hugegraph.service.space.GraphSpaceService; +import org.apache.hugegraph.structure.space.GraphSpace; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Ex; +import org.apache.hugegraph.util.UrlUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces") +public class GraphSpaceController extends BaseController { + private static final int DEFAULT_MEMORY_VALUE = 128; + private static final int DEFAULT_CPU_VALUE = 64; + + @Autowired + private GraphSpaceService graphSpaceService; + + @Autowired + private UserService userService; + + @Autowired + GraphsService graphsService; + @Autowired + HugeConfig config; + + private boolean isPdEnabled() { + return config.get(HubbleOptions.PD_ENABLED); + } + + @GetMapping("list") + public Object list() { + if (!isPdEnabled()) { + return ImmutableMap.of("graphspaces", + Collections.singletonList("DEFAULT")); + } + + List graphSpaces = + this.graphSpaceService.listAll(this.authClient(null, null)); + return ImmutableMap.of("graphspaces", graphSpaces); + } + + @GetMapping + public Object queryPage(@RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "create_time", required = false, + defaultValue = "") String createTime, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize, + @RequestParam(name = "all", required = false, + defaultValue = "false") boolean all) { + if (!isPdEnabled()) { + return ImmutableMap.of("records", Collections.emptyList(), + "total", 0); + } + if (all) { + return this.graphSpaceService.queryAllGs( + this.authClient(null, null), query, createTime); + } + return graphSpaceService.queryPage(this.authClient(null, null), + query, createTime, pageNo, pageSize); + } + + @GetMapping("{graphspace}/auth") + public Object isAuth(@PathVariable("graphspace") String graphSpace) { + if (!isPdEnabled()) { + return ImmutableMap.of("auth", false); + } + boolean isAuth = graphSpaceService.isAuth(this.authClient(null, null), + graphSpace); + return ImmutableMap.of("auth", isAuth); + } + + @GetMapping("{graphspace}") + public GraphSpaceEntity get(@PathVariable("graphspace") String graphspace) { + if (!isPdEnabled()) { + // Return a minimal stub entity for non-PD mode + GraphSpaceEntity stub = new GraphSpaceEntity(); + stub.setName(graphspace); + stub.setNickname(graphspace); + return stub; + } + HugeClient client = this.authClient(null, null); + // Get GraphSpace Info + return graphSpaceService.getWithAdmins(client, graphspace); + } + + @PostMapping + public Object add(@RequestBody GraphSpaceEntity graphSpaceEntity) { + E.checkArgument(isPdEnabled(), + "GraphSpace management is not supported in standalone mode"); + // Create GraphSpace + HugeClient client = this.authClient(null, null); + if (graphSpaceEntity.getCpuLimit() <= 0) { + graphSpaceEntity.setCpuLimit(DEFAULT_CPU_VALUE); + } + if (graphSpaceEntity.getMemoryLimit() <= 0) { + graphSpaceEntity.setMemoryLimit(DEFAULT_MEMORY_VALUE); + } + if (graphSpaceEntity.getComputeCpuLimit() <= 0) { + graphSpaceEntity.setComputeCpuLimit(DEFAULT_CPU_VALUE); + } + if (graphSpaceEntity.getComputeMemoryLimit() <= 0) { + graphSpaceEntity.setComputeCpuLimit(DEFAULT_MEMORY_VALUE); + } + + graphSpaceService.create(client, graphSpaceEntity.convertGraphSpace()); + + // Add GraphSpace Admin + graphSpaceEntity.graphspaceAdmin.forEach(u -> { + client.auth().addSpaceAdmin(u, graphSpaceEntity.getName()); + }); + + return get(graphSpaceEntity.getName()); + } + + @PutMapping("{graphspace}") + public GraphSpace update(@PathVariable("graphspace") String graphspace, + @RequestBody GraphSpaceEntity graphSpaceEntity) { + E.checkArgument(isPdEnabled(), + "GraphSpace management is not supported in standalone mode"); + + graphSpaceEntity.setName(graphspace); + + HugeClient client = this.authClient(null, null); + + // Update graphspace + graphSpaceService.update(client, graphSpaceEntity.convertGraphSpace()); + + // Update graphspace admin + ImmutableSet oldSpaceAdmins + = ImmutableSet.copyOf(userService.listGraphSpaceAdmin(client, + graphspace)); + ImmutableSet curSpaceAdmins + = ImmutableSet.copyOf(graphSpaceEntity.graphspaceAdmin); + + // a. Del + SetUtils.difference(oldSpaceAdmins, curSpaceAdmins).forEach(u -> { + client.auth().delSpaceAdmin(u, graphspace); + }); + // b. Add + SetUtils.difference(curSpaceAdmins, oldSpaceAdmins).forEach(u -> { + client.auth().addSpaceAdmin(u, graphspace); + }); + + return get(graphSpaceEntity.getName()); + } + + @DeleteMapping("{graphspace}") + public void delete(@PathVariable("graphspace") String graphspace) { + E.checkArgument(isPdEnabled(), + "GraphSpace management is not supported in standalone mode"); + E.checkArgument(StringUtils.isNotEmpty(graphspace), "graphspace " + + "must not null"); + + HugeClient client = this.authClient(null, null); + + // Delete graphspace admin + userService.listGraphSpaceAdmin(client, graphspace).forEach(u -> { + client.auth().delSpaceAdmin(u, graphspace); + }); + + // delete graphspace + graphSpaceService.delete(client, graphspace); + } + + @PostMapping("builtin") + public Object initBuiltIn(@RequestBody BuiltInEntity entity) { + E.checkArgument(isPdEnabled(), + "Built-in initialization is not supported in standalone mode"); + GraphConnection connection = new GraphConnection(); + + String url = this.getUrl(); + UrlUtil.Host host = UrlUtil.parseHost(url); + connection.setProtocol(host.getScheme()); + connection.setHost(host.getHost()); + connection.setPort(host.getPort()); + + connection.setCluster(config.get(HubbleOptions.PD_CLUSTER)); + connection.setRouteType(config.get(HubbleOptions.ROUTE_TYPE)); + connection.setPdPeers(config.get(HubbleOptions.PD_PEERS)); + connection.setGraphSpace(Constant.BUILT_IN); + connection.setToken(this.getToken()); + + HugeClient client = this.authClient(null, null); + Ex.check(userService.isSuperAdmin(client), "仅限系统管理员操作"); + if (entity.initSpace) { + graphSpaceService.initBuiltIn(client); + } + client.assignGraph(Constant.BUILT_IN, null); + graphsService.initBuiltIn(client, connection, entity); + return null; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/SchemaTemplateController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/SchemaTemplateController.java new file mode 100644 index 000000000..2e887a342 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/SchemaTemplateController.java @@ -0,0 +1,117 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.space; + +import java.util.List; + +import com.google.common.collect.ImmutableMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.space.SchemaTemplateService; +import org.apache.hugegraph.structure.space.SchemaTemplate; +import org.apache.hugegraph.util.E; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace" + + "}/schematemplates") +public class SchemaTemplateController extends BaseController { + @Autowired + SchemaTemplateService schemaTemplateService; + + @Autowired + HugeConfig config; + + private boolean isPdEnabled() { + return config.get(HubbleOptions.PD_ENABLED); + } + + @GetMapping("list") + public Object listName(@PathVariable("graphspace") String graphSpace) { + if (!isPdEnabled()) { + return ImmutableMap.of("schemas", java.util.Collections.emptyList()); + } + HugeClient client = this.authClient(graphSpace, null); + List names = schemaTemplateService.listName(client); + + return ImmutableMap.of("schemas", names); + } + + @GetMapping() + public Object list(@PathVariable("graphspace") String graphSpace, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + if (!isPdEnabled()) { + return ImmutableMap.of("records", java.util.Collections.emptyList(), + "total", 0); + } + HugeClient client = this.authClient(graphSpace, null); + return schemaTemplateService.queryPage(client, query, pageNo, pageSize); + } + + @GetMapping("{name}") + public Object get(@PathVariable("graphspace") String graphSpace, + @PathVariable("name") String name) { + HugeClient client = this.authClient(graphSpace, null); + return schemaTemplateService.get(client, name); + } + + @PostMapping + public Object create(@PathVariable("graphspace") String graphSpace, + @RequestBody SchemaTemplate schemaTemplate) { + E.checkArgument(isPdEnabled(), + "Schema template is not supported in standalone mode"); + HugeClient client = this.authClient(graphSpace, null); + + return schemaTemplateService.create(client, schemaTemplate); + } + + @DeleteMapping("{name}") + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("name") String name) { + HugeClient client = this.authClient(graphSpace, null); + schemaTemplateService.delete(client, name); + } + + @PutMapping("{name}") + public Object update(@PathVariable("graphspace") String graphSpace, + @PathVariable("name") String name, + @RequestBody SchemaTemplate schemaTemplate) { + HugeClient client = this.authClient(graphSpace, null); + schemaTemplate.name(name); + return schemaTemplateService.update(client, schemaTemplate); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/ServiceController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/ServiceController.java new file mode 100644 index 000000000..94e77c631 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/ServiceController.java @@ -0,0 +1,182 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.space; + +import com.google.common.collect.ImmutableMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.exception.ParameterizedException; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.service.space.OLTPServerService; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.structure.space.OLTPService; + +import java.util.List; + +@RestController +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/services/oltp") +public class ServiceController extends BaseController { + + @Autowired + OLTPServerService oltpService; + + @Autowired + HugeConfig config; + + private void checkPdMode() { + E.checkArgument(config.get(HubbleOptions.PD_ENABLED), + "Service management is not supported in standalone mode"); + } + + @GetMapping + public Object queryPage(@PathVariable("graphspace") String graphspace, + @RequestParam(name = "query", required = false, + defaultValue = "") String query, + @RequestParam(name = "page_no", required = false, + defaultValue = "1") int pageNo, + @RequestParam(name = "page_size", required = false, + defaultValue = "10") int pageSize) { + checkPdMode(); + try (HugeClient client = defaultClient(graphspace, null);) { + return oltpService.queryPage(client, query, pageNo, pageSize); + } catch (Throwable t) { + throw t; + } + } + + @GetMapping("{service}") + public Object get(@PathVariable("graphspace") String graphspace, + @PathVariable("service") String service) { + checkPdMode(); + try (HugeClient client = defaultClient(graphspace, null);) { + return oltpService.get(client, service); + } catch (Throwable t) { + throw t; + } + } + + @PostMapping + public Object create(@PathVariable("graphspace") String graphspace, + @RequestBody OLTPService serviceEntity) { + + checkPdMode(); + // return serviceEntity; + // TODO url or routetype + if (serviceEntity.getDepleymentType() == + OLTPService.DepleymentType.MANUAL) { + serviceEntity.setRouteType(null); + } else { + serviceEntity.setRouteType("NodePort"); + serviceEntity.setUrls(null); + } + + try (HugeClient client = defaultClient(graphspace, null);) { + return oltpService.create(client, serviceEntity); + } catch (Throwable t) { + throw t; + } + } + + @PutMapping("{service}") + public Object update(@PathVariable("graphspace") String graphspace, + @PathVariable("service") String service, + @RequestBody OLTPService serviceEntity) { + + checkPdMode(); + E.checkArgument(!serviceEntity.getDepleymentType() + .equals(OLTPService.DepleymentType.MANUAL), + "service.manual.disable.modify"); + + serviceEntity.setName(service); + serviceEntity.setRouteType("NodePort"); + serviceEntity.setUrls(null); + + try (HugeClient client = defaultClient(graphspace, null);) { + return oltpService.update(client, serviceEntity); + } catch (Throwable t) { + throw t; + } + } + + @DeleteMapping("{service}") + public void delete(@PathVariable("graphspace") String graphspace, + @PathVariable("service") String service) { + checkPdMode(); + if (("DEFAULT".equals(graphspace)) && ("DEFAULT".equals(service))) { + throw new ParameterizedException("Do not delete the service " + + "'DEFAULT' under the graphspace " + + "named 'DEFAULT'!"); + } + try (HugeClient client = defaultClient(graphspace, null);) { + oltpService.delete(client, service); + } catch (Throwable t) { + throw t; + } + } + + @GetMapping("{service}/start") + public void start(@PathVariable("graphspace") String graphspace, + @PathVariable("service") String service) { + + checkPdMode(); + try (HugeClient client = defaultClient(graphspace, null);) { + oltpService.start(client, service); + } catch (Throwable t) { + throw t; + } + } + + @GetMapping("{service}/stop") + public void stop(@PathVariable("graphspace") String graphspace, + @PathVariable("service") String service) { + + checkPdMode(); + try (HugeClient client = defaultClient(graphspace, null);) { + oltpService.stop(client, service); + } catch (Throwable t) { + throw t; + } + } + + @GetMapping("options/list") + public Object configOptions(@PathVariable("graphspace") String graphspace) { + checkPdMode(); + try (HugeClient client = defaultClient(graphspace, null);) { + List fields = oltpService.configOptionList(client); + + return ImmutableMap.of("options", fields); + } catch (Throwable t) { + throw t; + } + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/VermeerController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/VermeerController.java new file mode 100644 index 000000000..99b6b9b3d --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/VermeerController.java @@ -0,0 +1,136 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.space; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.loader.util.JsonUtil; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.space.VermeerService; +import org.apache.hugegraph.util.HubbleUtil; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.extern.log4j.Log4j2; + +@RestController +@RequestMapping(Constant.API_VERSION + "vermeer") +@Log4j2 +public class VermeerController extends BaseController { + + @Autowired + VermeerService vermeerService; + @Autowired + private HugeConfig config; + + private static final String GRAPH_LOADED = "loaded"; + + @GetMapping + public Map getStatus(@RequestParam(value = "check", + required = false, + defaultValue = "false") + boolean check) { + String username = this.getUser(); + String password = this.getCredentialPassword(); + return vermeerService.getVermeer(username, password, check); + } + + @PostMapping("task") + public void load(@RequestBody JsonLoad body) { + String graphspace = body.graphspace; + String graph = body.graph; + String vGraph = vermeerService.convert2VG(graphspace, graph); + HugeClient client = this.authClient(null, null); + + Map graphInfo = HubbleUtil.uncheckedCast( + client.vermeer().getGraphInfoByName(vGraph).get("graph")); + + // if (graphInfo != null && !graphInfo.isEmpty()) { + // E.checkArgument(body.force || !GRAPH_LOADED.equals(graphInfo.get( + // "status")), "graph already loaded"); + // vermeerService.deleteGraph(client, graphspace, graph); + // } + + Map params = new HashMap<>(); + String pdPeers = config.get(HubbleOptions.PD_PEERS); + String pdJson = JsonUtil.toJson(Arrays.asList(pdPeers.split(","))); + params.put("load.parallel", "50"); + params.put("load.type", "hugegraph"); + params.put("load.hg_pd_peers", pdJson); + params.put("load.use_outedge", "1"); + params.put("load.use_out_degree", "1"); + params.put("load.hugegraph_name", graphspace + "/" + graph + "/g"); + params.put("load.hugegraph_username", this.getUser()); + params.put("load.hugegraph_password", this.getCredentialPassword()); + + if (body.params != null) { + params.putAll(body.analyze()); + } + + vermeerService.load(client, graphspace, graph, body.taskType, params); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Interrupted while waiting for the Vermeer load task", e); + } + } + + @DeleteMapping("{name}") + public void deleteGraph(@RequestBody Map body) { + String graphspace = body.get("graphspace"); + String graph = body.get("graph"); + HugeClient client = this.authClient(graphspace, graph); + vermeerService.deleteGraph(client, graphspace, graph); + } + + private static class JsonLoad { + @JsonProperty("graphspace") + public String graphspace; + @JsonProperty("graph") + public String graph; + @JsonProperty("force") + public boolean force = false; + @JsonProperty("params") + public Map params; + + @JsonProperty("task_type") + public String taskType; + + public Map analyze() { + for (Map.Entry entry : params.entrySet()) { + entry.setValue(entry.getValue().toString()); + } + return params; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/task/AsyncTaskController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/task/AsyncTaskController.java index 989cca5b7..99d70d0af 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/task/AsyncTaskController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/task/AsyncTaskController.java @@ -18,10 +18,11 @@ package org.apache.hugegraph.controller.task; -import java.util.List; - +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.service.algorithm.AsyncTaskService; import org.apache.hugegraph.structure.Task; @@ -34,13 +35,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.baomidou.mybatisplus.core.metadata.IPage; - -import lombok.extern.log4j.Log4j2; +import java.util.List; @Log4j2 @RestController -@RequestMapping(Constant.API_VERSION + "graph-connections/{connId}/async-tasks") +@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" + + "/{graph}/async-tasks") public class AsyncTaskController extends BaseController { private final AsyncTaskService service; @@ -51,9 +51,11 @@ public AsyncTaskController(AsyncTaskService service) { } @GetMapping("{id}") - public Task get(@PathVariable("connId") int connId, + public Task get(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") int id) { - Task task = this.service.get(connId, id); + HugeClient client = this.authClient(graphSpace, graph); + Task task = this.service.get(client, id); if (task == null) { throw new ExternalException("async.task.not-exist.id", id); } @@ -61,9 +63,11 @@ public Task get(@PathVariable("connId") int connId, } @PostMapping("cancel/{id}") - public Task cancel(@PathVariable("connId") int connId, + public Task cancel(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @PathVariable("id") int id) { - Task task = this.service.cancel(connId, id); + HugeClient client = this.authClient(graphSpace, graph); + Task task = this.service.cancel(client, id); if (task == null) { throw new ExternalException("async.task.not-exist.id", id); } @@ -71,45 +75,51 @@ public Task cancel(@PathVariable("connId") int connId, } @GetMapping("ids") - public List list(@PathVariable("connId") int connId, + public List list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam("ids") List taskIds) { - return this.service.list(connId, taskIds); + HugeClient client = this.authClient(graphSpace, graph); + return this.service.list(client, taskIds); } @GetMapping - public IPage list(@PathVariable("connId") int connId, + public IPage list(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam(name = "page_no", required = false, defaultValue = "1") - int pageNo, + int pageNo, @RequestParam(name = "page_size", required = false, defaultValue = "10") - int pageSize, + int pageSize, @RequestParam(name = "content", required = false, defaultValue = "") - String content, + String content, @RequestParam(name = "type", required = false, defaultValue = "") - String type, + String type, @RequestParam(name = "status", required = false, defaultValue = "") - String status) { - return this.service.list(connId, pageNo, pageSize, content, type, status); + String status) { + HugeClient client = this.authClient(graphSpace, graph); + return this.service.list(client, pageNo, pageSize, content, type, status); } @DeleteMapping - public void delete(@PathVariable("connId") int connId, + public void delete(@PathVariable("graphspace") String graphSpace, + @PathVariable("graph") String graph, @RequestParam("ids") List ids) { + HugeClient client = this.authClient(graphSpace, graph); for (int id : ids) { - Task task = this.service.get(connId, id); + Task task = this.service.get(client, id); if (task == null) { throw new ExternalException("async.task.not-exist.id", id); } - this.service.remove(connId, id); + this.service.remove(client, id); } } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/GraphConnection.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/GraphConnection.java index 3957f836f..21cf472e6 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/GraphConnection.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/GraphConnection.java @@ -18,29 +18,28 @@ package org.apache.hugegraph.entity; -import java.util.Date; - -import org.apache.hugegraph.annotation.MergeProperty; -import org.apache.hugegraph.common.Identifiable; -import org.apache.hugegraph.common.Mergeable; - import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.annotation.MergeProperty; +import org.apache.hugegraph.common.Identifiable; +import org.apache.hugegraph.common.Mergeable; + +import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor @Builder -@JsonIgnoreProperties({"protocol", "truststore_file", "truststore_password"}) +@JsonIgnoreProperties({"protocol", "truststore_file", "truststore_password", + "password", "token"}) @TableName(value = "graph_connection", autoResultMap = true) public class GraphConnection implements Identifiable, Mergeable { @@ -53,6 +52,20 @@ public class GraphConnection implements Identifiable, Mergeable { @JsonProperty("name") private String name; + @JsonProperty("route_type") + private String routeType; + + @JsonProperty("pd_peers") + private String pdPeers; + + @MergeProperty + @JsonProperty("cluster") + private String cluster; + + @MergeProperty + @JsonProperty("graphspace") + private String graphSpace; + @MergeProperty @JsonProperty("graph") private String graph; @@ -77,6 +90,10 @@ public class GraphConnection implements Identifiable, Mergeable { @JsonProperty("password") private String password; + @MergeProperty + @JsonProperty("token") + private String token; + @MergeProperty(useNew = false) @JsonProperty("enabled") private Boolean enabled; @@ -103,4 +120,8 @@ public class GraphConnection implements Identifiable, Mergeable { @MergeProperty(useNew = false) @JsonProperty("truststore_password") private String trustStorePassword; + + public String getGraphId() { + return String.format("%s/%s/%s", cluster, graphSpace, graph); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/AdamicadarEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/AdamicadarEntity.java new file mode 100644 index 000000000..ffe63f9f4 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/AdamicadarEntity.java @@ -0,0 +1,51 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AdamicadarEntity { + @JsonProperty("vertex") + private Object vertex; + + @JsonProperty("other") + private Object other; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("limit") + private int limit; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/AllShortestPathsEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/AllShortestPathsEntity.java new file mode 100644 index 000000000..e8e0f8317 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/AllShortestPathsEntity.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AllShortestPathsEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("target") + private Object target; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("skip_degree") + private long skipDegree; + + @JsonProperty("capacity") + private long capacity = Traverser.DEFAULT_CAPACITY; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/CrossPointsEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/CrossPointsEntity.java new file mode 100644 index 000000000..818065666 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/CrossPointsEntity.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CrossPointsEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("target") + private Object target; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("capacity") + private long capacity = Traverser.DEFAULT_CAPACITY; + + @JsonProperty("limit") + private long limit = Traverser.DEFAULT_PATHS_LIMIT; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/JaccardSimilarityEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/JaccardSimilarityEntity.java new file mode 100644 index 000000000..63a4afaea --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/JaccardSimilarityEntity.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import org.apache.hugegraph.structure.constant.Direction; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class JaccardSimilarityEntity { + @JsonProperty("vertex") + private Object vertex; + + @JsonProperty("other") + private Object other; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/KneighborEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/KneighborEntity.java new file mode 100644 index 000000000..21703e145 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/KneighborEntity.java @@ -0,0 +1,51 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class KneighborEntity { + @JsonProperty("source") + private Object source; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("limit") + private int limit; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/KoutEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/KoutEntity.java new file mode 100644 index 000000000..01adc09ac --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/KoutEntity.java @@ -0,0 +1,61 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class KoutEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("label") + private String label; + + @JsonProperty("nearest") + private boolean nearest; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("capacity") + private long capacity; + + @JsonProperty("limit") + private int limit; + + @JsonProperty("algorithm") + private String algorithm; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/OlapEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/OlapEntity.java new file mode 100644 index 000000000..646137fc1 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/OlapEntity.java @@ -0,0 +1,43 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OlapEntity { + + @JsonProperty("algorithm") + private String algorithm; + + @JsonProperty("worker") + private long worker; + + @JsonProperty("params") + private Map params; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/PathsEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/PathsEntity.java new file mode 100644 index 000000000..e01c5114f --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/PathsEntity.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PathsEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("target") + private Object target; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("capacity") + private long capacity = Traverser.DEFAULT_CAPACITY; + + @JsonProperty("limit") + private long limit = Traverser.DEFAULT_PATHS_LIMIT; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/RaysEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/RaysEntity.java new file mode 100644 index 000000000..379b34df2 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/RaysEntity.java @@ -0,0 +1,55 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RaysEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("capacity") + private long capacity = Traverser.DEFAULT_CAPACITY; + + @JsonProperty("limit") + private long limit = Traverser.DEFAULT_PATHS_LIMIT; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ResourceallocationEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ResourceallocationEntity.java new file mode 100644 index 000000000..daffb2b7e --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ResourceallocationEntity.java @@ -0,0 +1,51 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ResourceallocationEntity { + @JsonProperty("vertex") + private Object vertex; + + @JsonProperty("other") + private Object other; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("limit") + private int limit; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/RingsEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/RingsEntity.java new file mode 100644 index 000000000..8d3cbffbb --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/RingsEntity.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RingsEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("source_in_ring") + private boolean sourceInRing; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("capacity") + private long capacity; + + @JsonProperty("limit") + private int limit; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/SameNeighborsEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/SameNeighborsEntity.java new file mode 100644 index 000000000..8892099a4 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/SameNeighborsEntity.java @@ -0,0 +1,52 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SameNeighborsEntity { + + @JsonProperty("vertex") + private Object vertex; + + @JsonProperty("other") + private Object other; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("limit") + private int limit; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ShortestPath.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ShortestPath.java deleted file mode 100644 index e70b366f0..000000000 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ShortestPath.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; - -import org.apache.hugegraph.structure.constant.Direction; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ShortestPath { - - @JsonProperty("source") - private Object source; - - @JsonProperty("target") - private Object target; - - @JsonProperty("direction") - private Direction direction; - - @JsonProperty("label") - private String label; - - @JsonProperty("max_depth") - private int maxDepth; - - @JsonProperty("max_degree") - private long maxDegree; - - @JsonProperty("skip_degree") - private long skipDegree; - - @JsonProperty("capacity") - private long capacity; -} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ShortestPathEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ShortestPathEntity.java new file mode 100644 index 000000000..29c506798 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/ShortestPathEntity.java @@ -0,0 +1,59 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ShortestPathEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("target") + private Object target; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_depth") + private int maxDepth; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("skip_degree") + private long skipDegree; + + @JsonProperty("capacity") + private long capacity = Traverser.DEFAULT_CAPACITY; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/SingleSourceShortestPathEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/SingleSourceShortestPathEntity.java new file mode 100644 index 000000000..b053f5c5c --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/SingleSourceShortestPathEntity.java @@ -0,0 +1,61 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SingleSourceShortestPathEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("weight") + private String weight; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("skip_degree") + private long skipDegree; + + @JsonProperty("capacity") + private long capacity = Traverser.DEFAULT_CAPACITY; + + @JsonProperty("with_vertex") + private boolean withVertex; + + @JsonProperty("limit") + private long limit = Traverser.DEFAULT_PATHS_LIMIT; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/WeightedShortestPathEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/WeightedShortestPathEntity.java new file mode 100644 index 000000000..8d51e8ced --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/algorithm/WeightedShortestPathEntity.java @@ -0,0 +1,61 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.Traverser; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WeightedShortestPathEntity { + + @JsonProperty("source") + private Object source; + + @JsonProperty("target") + private Object target; + + @JsonProperty("direction") + private Direction direction; + + @JsonProperty("weight") + private String weight; + + @JsonProperty("label") + private String label; + + @JsonProperty("max_degree") + private long maxDegree = Traverser.DEFAULT_MAX_DEGREE; + + @JsonProperty("skip_degree") + private long skipDegree; + + @JsonProperty("capacity") + private long capacity = Traverser.DEFAULT_CAPACITY; + + @JsonProperty("with_vertex") + private boolean withVertex; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/AccessEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/AccessEntity.java new file mode 100644 index 000000000..8ffb6c54f --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/AccessEntity.java @@ -0,0 +1,68 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.auth; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.hugegraph.structure.auth.HugePermission; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AccessEntity { + + @JsonProperty("target_id") + private String targetId; + + @JsonProperty("target_name") + private String targetName; + + @JsonProperty("role_id") + private String roleId; + + @JsonProperty("role_name") + private String roleName; + + @JsonProperty("graphspace") + private String graphSpace; + + @JsonProperty("graph") + private String graph; + + @JsonProperty("permissions") + private Set permissions; + + @JsonProperty("target_description") + protected String description; + + @JsonProperty("target_resources") + protected List> resources; + + public void addPermission(HugePermission p) { + permissions.add(p); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/BelongEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/BelongEntity.java new file mode 100644 index 000000000..e5ca9718f --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/BelongEntity.java @@ -0,0 +1,57 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.apache.hugegraph.common.Identifiable; + +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BelongEntity implements Identifiable { + + @JsonProperty("id") + private String id; + + @JsonProperty("user_id") + private String userId; + + @JsonProperty("user_name") + private String userName; + + @JsonProperty("role_id") + private String roleId; + + @JsonProperty("role_name") + private String roleName; + + @JsonProperty("user_description") + private String description; + + @JsonProperty("user_create") + protected Date create; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/PasswordEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/PasswordEntity.java new file mode 100644 index 000000000..1a4989918 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/PasswordEntity.java @@ -0,0 +1,41 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PasswordEntity { + + @JsonProperty("username") + private String username; + + @JsonProperty("oldpwd") + private String oldpwd; + + @JsonProperty("newpwd") + private String newpwd; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/RoleEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/RoleEntity.java new file mode 100644 index 000000000..5da056121 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/RoleEntity.java @@ -0,0 +1,40 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.apache.hugegraph.common.Identifiable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RoleEntity implements Identifiable { + + @JsonProperty("role_id") + private String id; + + @JsonProperty("role_name") + private String name; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserEntity.java new file mode 100644 index 000000000..e17cd22aa --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserEntity.java @@ -0,0 +1,82 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.apache.hugegraph.common.Identifiable; + +import java.util.Date; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserEntity implements Identifiable { + + @JsonProperty("id") + private String id; + + @JsonProperty("user_name") + private String name; + + @JsonProperty("user_nickname") + private String nickname; + + @JsonProperty("user_email") + private String email; + + @JsonProperty("user_password") + private String password; + + @JsonProperty("user_phone") + private String phone; + + @JsonProperty("user_avatar") + private String avatar; + + @JsonProperty("user_description") + private String description; + + @JsonProperty("user_create") + protected Date create; + @JsonProperty("user_update") + protected Date update; + @JsonProperty("user_creator") + protected String creator; + + @JsonProperty("adminSpaces") + protected List adminSpaces; + + @JsonProperty("resSpaces") + protected List resSpaces; + + @JsonProperty("spacenum") + protected Integer spacenum; + + @JsonProperty("is_superadmin") + private boolean isSuperadmin; +} + + diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserView.java new file mode 100644 index 000000000..5b1fc8195 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserView.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.auth; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserView { + + @JsonProperty("user_id") + private String id; + + @JsonProperty("user_name") + private String name; + + @JsonProperty("roles") + private List roles; + + public void addRole(RoleEntity g) { + this.roles.add(g); + } + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/WhiteIpListEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/WhiteIpListEntity.java new file mode 100644 index 000000000..8dad0b607 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/WhiteIpListEntity.java @@ -0,0 +1,39 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WhiteIpListEntity { + @JsonProperty("action") + private String action; + + @JsonProperty("ips") + private List ips; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java index 01ad2494d..fc1c4363e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java @@ -19,14 +19,21 @@ package org.apache.hugegraph.entity.enums; import com.baomidou.mybatisplus.core.enums.IEnum; +import org.apache.hugegraph.exception.InternalException; + +import java.util.HashSet; +import java.util.Set; public enum ExecuteType implements IEnum { GREMLIN(0), ALGORITHM(1), - - GREMLIN_ASYNC(5); + CYPHER(2), + CYPHER_ASYNC(4), + GREMLIN_ASYNC(5), + GREMLIN_ALL(6), + CYPHER_ALL(7); private byte code; @@ -39,4 +46,24 @@ public enum ExecuteType implements IEnum { public Byte getValue() { return this.code; } + + public static boolean isSingle(int type) { + return type < GREMLIN_ALL.getValue(); + } + + public static Set getMatchedTypes(int type) { + Set types = new HashSet<>(); + if (isSingle(type)) { + types.add(type); + } else if (type == GREMLIN_ALL.getValue()) { + types.add(GREMLIN_ASYNC.getValue().intValue()); + types.add(GREMLIN.getValue().intValue()); + } else if (type == CYPHER_ALL.getValue()) { + types.add(CYPHER.getValue().intValue()); + types.add(CYPHER_ASYNC.getValue().intValue()); + } else { + throw new InternalException("executehistory type invalid"); + } + return types; + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java index 678ed6e21..6db79f45e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java @@ -30,7 +30,10 @@ public enum LoadStatus implements IEnum { PAUSED(3), - STOPPED(4); + STOPPED(4), + + // No Used + INIT(5); private byte code; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/EdgeEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/EdgeEntity.java index cb3824ef6..9e955dcd4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/EdgeEntity.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/EdgeEntity.java @@ -18,16 +18,14 @@ package org.apache.hugegraph.entity.graph; -import java.util.Map; - -import org.apache.hugegraph.common.Identifiable; - import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.common.Identifiable; + +import java.util.Map; @Data @NoArgsConstructor diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/VertexEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/VertexEntity.java index 9cf6fd3fb..215c226be 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/VertexEntity.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/VertexEntity.java @@ -21,7 +21,6 @@ import java.util.Map; import org.apache.hugegraph.common.Identifiable; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/VertexQueryEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/VertexQueryEntity.java new file mode 100644 index 000000000..4b8f93173 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graph/VertexQueryEntity.java @@ -0,0 +1,73 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.graph; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; +import org.apache.hugegraph.structure.graph.Vertex; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +public class VertexQueryEntity extends Vertex { + @JsonProperty("statistics") + private Map statistics; + + public VertexQueryEntity(String label) { + super(label); + this.statistics = new HashMap<>(); + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public static VertexQueryEntity fromVertex(Vertex vertex) { + VertexQueryEntity vertexQE = + new VertexQueryEntity(vertex.label()); + vertexQE.id(vertex.id()); + vertexQE.setProperties(vertex.properties()); + return vertexQE; + } + + public static Collection fromVertices(Collection vertices) { + Collection vertexQueryEntities = new ArrayList<>(); + for (Vertex vertex: vertices) { + vertexQueryEntities.add(fromVertex(vertex)); + } + return vertexQueryEntities; + } + + public Vertex convertVertex() { + // get Vertex instance from VertexQueryEntity + return this; + } + + @Override + public String toString() { + return String.format("{id=%s, label=%s, properties=%s, statistics=%s}", + this.id(), + this.label(), this.properties(), this.statistics); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphCloneEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphCloneEntity.java new file mode 100644 index 000000000..2f9ed054b --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphCloneEntity.java @@ -0,0 +1,67 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.graphs; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GraphCloneEntity { + @JsonProperty("graphspace") + public String graphSpace; + @JsonProperty("name") + public String name; + @JsonProperty("nickname") + public String nickname; + @JsonProperty("load_data") + public int loadData = 0; + + public Map convertMap(String graphSpace, String graph) { + Map params = new HashMap<>(4); + + String name = (this.name == null) ? graph : this.name; + String nickname = (this.nickname == null) ? graph : this.nickname; + String space = (this.graphSpace == null) ? graphSpace : this.graphSpace; + + Map configs = new HashMap<>(); + configs.put("backend", "hstore"); + configs.put("serializer", "binary"); + configs.put("name", name); + configs.put("nickname", nickname); + configs.put("search.text_analyzer", "jieba"); + configs.put("search.text_analyzer_mode", "INDEX"); + + params.put("configs", configs); + params.put("create", true); + params.put("init_schema", true); + params.put("graphspace", space); + params.put("load_data", (boolean) (this.loadData == 1)); + return params; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphCreateEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphCreateEntity.java new file mode 100644 index 000000000..87655aa2a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphCreateEntity.java @@ -0,0 +1,34 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.graphs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GraphCreateEntity { + + private String nickname; + private String schema; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphEntity.java new file mode 100644 index 000000000..70db00441 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphEntity.java @@ -0,0 +1,34 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.graphs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GraphEntity { + public String cluster; + public String graphSpace; + public String graph; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphStatisticsEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphStatisticsEntity.java new file mode 100644 index 000000000..0dfee7287 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphStatisticsEntity.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.graphs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GraphStatisticsEntity { + @JsonProperty("storage") + public long storage; + @JsonProperty("vertex_count") + public String vertexCount; + @JsonProperty("edge_count") + public String edgeCount; + @JsonProperty("update_time") + public String updateTime; + @JsonProperty("vertices") + public Map vertices; + @JsonProperty("edges") + public Map edges; + + public static GraphStatisticsEntity emptyEntity() { + GraphStatisticsEntity empty = new GraphStatisticsEntity(); + empty.setStorage(0); + empty.setVertexCount("0"); + empty.setEdgeCount("0"); + empty.setVertices(ImmutableMap.of()); + empty.setEdges(ImmutableMap.of()); + empty.setUpdateTime("1970-01-01 00:00:00.000"); + return empty; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphUpdateEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphUpdateEntity.java new file mode 100644 index 000000000..49b771fb3 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/graphs/GraphUpdateEntity.java @@ -0,0 +1,31 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.graphs; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GraphUpdateEntity { + + private String nickname; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/Datasource.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/Datasource.java new file mode 100644 index 000000000..8d62df388 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/Datasource.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.load; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@TableName(value = "datasource", autoResultMap = true) +public class Datasource { + + @TableId(type = IdType.AUTO) + @JsonProperty("datasource_id") + private Integer id; + + @TableField("datasource_name") + @JsonProperty("datasource_name") + private String datasourceName; + + @TableField(value = "datasource_config", typeHandler = JacksonTypeHandler.class) + @JsonProperty("datasource_config") + private Map datasourceConfig; + + @TableField("creator") + @JsonProperty("creator") + private String creator; + + @TableField("create_time") + @JsonProperty("create_time") + private Date createTime; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/EdgeMapping.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/EdgeMapping.java index 64a9d0365..29e5e8a1a 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/EdgeMapping.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/EdgeMapping.java @@ -18,16 +18,14 @@ package org.apache.hugegraph.entity.load; -import java.util.List; - -import org.apache.hugegraph.annotation.MergeProperty; - import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.apache.hugegraph.annotation.MergeProperty; + +import java.util.List; @Getter @NoArgsConstructor diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ElementMapping.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ElementMapping.java index 7c9ac7f6c..fc6fe17cd 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ElementMapping.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ElementMapping.java @@ -18,18 +18,16 @@ package org.apache.hugegraph.entity.load; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.apache.hugegraph.annotation.MergeProperty; -import org.apache.hugegraph.common.Mergeable; - import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.annotation.MergeProperty; +import org.apache.hugegraph.common.Mergeable; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @Data @NoArgsConstructor diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FieldMappingItem.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FieldMappingItem.java index b01be289a..892f0aeba 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FieldMappingItem.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FieldMappingItem.java @@ -19,7 +19,6 @@ package org.apache.hugegraph.entity.load; import org.apache.hugegraph.annotation.MergeProperty; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileMapping.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileMapping.java index 5f23c2135..de3edb6eb 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileMapping.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileMapping.java @@ -30,7 +30,6 @@ import org.apache.hugegraph.handler.VertexMappingTypeHandler; import org.apache.hugegraph.util.HubbleUtil; import org.apache.hugegraph.util.SerializeUtil; - import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -62,6 +61,14 @@ public class FileMapping { @JsonIgnore private Integer connId; + @TableField("graphspace") + @JsonIgnore + private String graphSpace; + + @TableField("graph") + @JsonIgnore + private String graph; + @TableField(value = "job_id") @MergeProperty @JsonProperty("job_id") @@ -121,14 +128,36 @@ public long getTotalSizeBytes() { return this.totalSize; } + public FileMapping(String graphSpace, String graph, String name, + String path) { + this(graphSpace, graph, name, path, HubbleUtil.nowDate()); + } + public FileMapping(int connId, String name, String path) { this(connId, name, path, HubbleUtil.nowDate()); } - public FileMapping(int connId, String name, String path, - Date lastAccessTime) { + public FileMapping(int connId, String name, String path, Date lastAccessTime) { this.id = null; this.connId = connId; + this.graphSpace = null; + this.graph = null; + this.name = name; + this.path = path; + this.fileSetting = new FileSetting(); + this.vertexMappings = new LinkedHashSet<>(); + this.edgeMappings = new LinkedHashSet<>(); + this.loadParameter = new LoadParameter(); + this.createTime = lastAccessTime; + this.updateTime = lastAccessTime; + } + + public FileMapping(String graphSpace, String graph, String name, String path, + Date lastAccessTime) { + this.id = null; + this.connId = null; + this.graphSpace = graphSpace; + this.graph = graph; this.name = name; this.path = path; this.fileSetting = new FileSetting(); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileSetting.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileSetting.java index c682f8f61..a90bcb078 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileSetting.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileSetting.java @@ -18,13 +18,6 @@ package org.apache.hugegraph.entity.load; -import java.io.IOException; -import java.util.List; - -import org.apache.hugegraph.annotation.MergeProperty; -import org.apache.hugegraph.common.Constant; -import org.apache.hugegraph.common.Mergeable; - import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -34,11 +27,16 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.annotation.MergeProperty; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Mergeable; + +import java.io.IOException; +import java.util.List; @Data @NoArgsConstructor @@ -80,7 +78,7 @@ public class FileSetting implements Mergeable { @MergeProperty @JsonProperty("skipped_line") - private String skippedLine = "(^#|^//).*|"; + private String skippedLine = "(^#|^//).*"; @MergeProperty @JsonProperty("list_format") @@ -118,7 +116,7 @@ protected DelimiterDeserializer() { @Override public String deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException { + throws IOException { String delimiter = jsonParser.getText(); if ("\\t".equals(delimiter)) { return "\t"; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileUploadResult.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileUploadResult.java index 967c2da6f..6e2fee5c5 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileUploadResult.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/FileUploadResult.java @@ -19,7 +19,6 @@ package org.apache.hugegraph.entity.load; import org.apache.hugegraph.util.SerializeUtil; - import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -58,6 +57,6 @@ public enum Status { FAILURE, - SUSPEND + SUSPEND; } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManager.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManager.java index 283000905..03d4e4c22 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManager.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManager.java @@ -18,24 +18,22 @@ package org.apache.hugegraph.entity.load; -import java.util.Date; - -import org.apache.hugegraph.annotation.MergeProperty; -import org.apache.hugegraph.entity.enums.JobStatus; -import org.apache.hugegraph.util.SerializeUtil; - import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import org.apache.hugegraph.annotation.MergeProperty; +import org.apache.hugegraph.entity.enums.JobStatus; +import org.apache.hugegraph.util.SerializeUtil; + +import java.util.Date; @Data @NoArgsConstructor @@ -55,6 +53,16 @@ public class JobManager { @JsonProperty("conn_id") private Integer connId; + @TableField(value = "graphspace") + @MergeProperty + @JsonProperty("graphspace") + private String graphSpace; + + @TableField(value = "graph") + @MergeProperty + @JsonProperty("graph") + private String graph; + @TableField(value = "job_name") @MergeProperty @JsonProperty("job_name") diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerItem.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerItem.java index 28de6ebe4..d489928cf 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerItem.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerItem.java @@ -20,7 +20,6 @@ import org.apache.hugegraph.annotation.MergeProperty; import org.apache.hugegraph.util.SerializeUtil; - import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerReasonResult.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerReasonResult.java index 55a2717ca..7a8489d6e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerReasonResult.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/JobManagerReasonResult.java @@ -19,7 +19,6 @@ package org.apache.hugegraph.entity.load; import org.apache.hugegraph.annotation.MergeProperty; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ListFormat.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ListFormat.java index edcf2404c..3d6c304fd 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ListFormat.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ListFormat.java @@ -18,14 +18,13 @@ package org.apache.hugegraph.entity.load; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.hugegraph.loader.constant.Constants; + import java.util.Collections; import java.util.HashSet; import java.util.Set; -import org.apache.hugegraph.loader.constant.Constants; - -import com.fasterxml.jackson.annotation.JsonProperty; - public final class ListFormat { private static final String DEFAULT_START_SYMBOL = ""; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadParameter.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadParameter.java index 1da3af183..ab0b88803 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadParameter.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadParameter.java @@ -18,15 +18,13 @@ package org.apache.hugegraph.entity.load; -import org.apache.hugegraph.annotation.MergeProperty; -import org.apache.hugegraph.common.Mergeable; - import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.annotation.MergeProperty; +import org.apache.hugegraph.common.Mergeable; @Data @NoArgsConstructor diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadTask.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadTask.java index 16422afab..7c49ee653 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadTask.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/LoadTask.java @@ -32,7 +32,6 @@ import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.HubbleUtil; import org.apache.hugegraph.util.SerializeUtil; - import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -78,6 +77,16 @@ public class LoadTask implements Runnable { @JsonProperty("conn_id") private Integer connId; + @TableField(value = "graphspace") + @MergeProperty + @JsonProperty("graphspace") + private String graphSpace; + + @TableField(value = "graph") + @MergeProperty + @JsonProperty("graph") + private String graph; + @TableField(value = "job_id") @MergeProperty @JsonProperty("job_id") @@ -144,6 +153,8 @@ public LoadTask(LoadOptions options, GraphConnection connection, this.finished = false; this.id = null; this.connId = connection.getId(); + this.graphSpace = connection.getGraphSpace(); + this.graph = connection.getGraph(); this.jobId = mapping.getJobId(); this.fileId = mapping.getId(); this.fileName = mapping.getName(); @@ -156,13 +167,14 @@ public LoadTask(LoadOptions options, GraphConnection connection, this.lastDuration = 0L; this.currDuration = 0L; this.createTime = HubbleUtil.nowDate(); + + this.loader = new HugeGraphLoader(this.options); } @Override public void run() { Ex.check(this.options != null, "The load options shouldn't be null"); log.info("LoadTask is start running : {}", this.id); - this.loader = new HugeGraphLoader(this.options); boolean noError; try { @@ -182,7 +194,12 @@ public void run() { this.status = LoadStatus.FAILED; } } - this.fileReadLines = this.context().newProgress().totalInputRead(); + // Restore file read lines progress tracking + long readLines = this.context().newProgress().totalInputRead(); + if (readLines == 0L) { + readLines = this.context().oldProgress().totalInputRead(); + } + this.fileReadLines = readLines; this.lastDuration += this.context().summary().totalTime(); this.currDuration = 0L; } finally { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/NullValues.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/NullValues.java index 874b8f13a..979003e45 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/NullValues.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/NullValues.java @@ -21,7 +21,6 @@ import java.util.Set; import org.apache.hugegraph.annotation.MergeProperty; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ValueMappingItem.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ValueMappingItem.java index 9bfe1d917..5a72f1444 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ValueMappingItem.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/ValueMappingItem.java @@ -21,7 +21,6 @@ import java.util.List; import org.apache.hugegraph.annotation.MergeProperty; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/VertexMapping.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/VertexMapping.java index 7c6f394fd..5c87fbab4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/VertexMapping.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/load/VertexMapping.java @@ -21,7 +21,6 @@ import java.util.List; import org.apache.hugegraph.annotation.MergeProperty; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -39,6 +38,10 @@ public class VertexMapping extends ElementMapping { @JsonProperty("id_fields") private List idFields; + public VertexMapping(String s, boolean b) { + // TODO Rmving? + } + @Override public boolean equals(Object object) { if (!(object instanceof VertexMapping)) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/op/AuditEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/op/AuditEntity.java new file mode 100644 index 000000000..a8ed6a67c --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/op/AuditEntity.java @@ -0,0 +1,73 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.op; + +import org.apache.hugegraph.util.JsonUtil; +import org.apache.hugegraph.util.HubbleUtil; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuditEntity { + @JsonProperty("audit_datetime") + private String datetime; + + @JsonProperty("audit_operation") + private String operation; + + @JsonProperty("audit_action") + private String action; + + @JsonProperty("audit_service") + private String service; + + @JsonProperty("audit_graphspace") + private String graphSpace; + + @JsonProperty("audit_graph") + private String graph; + + @JsonProperty("audit_level") + private String level; + + @JsonProperty("audit_user") + private String user; + + @JsonProperty("audit_ip") + private String ip; + + @JsonProperty("audit_result") + private String result = "Success"; + + public static AuditEntity fromMap(Map source) { + Map jsonData = + HubbleUtil.uncheckedCast(source.get("json")); + return JsonUtil.fromJson(JsonUtil.toJson(jsonData), AuditEntity.class); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/op/LogEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/op/LogEntity.java new file mode 100644 index 000000000..d5a13ec8e --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/op/LogEntity.java @@ -0,0 +1,69 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.op; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hugegraph.util.ESUtil; + +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class LogEntity { + @JsonProperty("log_datetime") + private String datetime; + + @JsonProperty("log_service") + private String service; + + @JsonProperty("log_host") + private String host; + + @JsonProperty("log_level") + private String level; + + @JsonProperty("log_message") + private String message; + + public static LogEntity fromMap(Map map) { + LogEntity logEntity = new LogEntity(); + logEntity.setDatetime( + ESUtil.parseTimestamp( + (String) ESUtil.getValueByPath(map, "@timestamp".split("\\.")))); + logEntity.setHost( + (String) ESUtil.getValueByPath(map, "host.name".split("\\."))); + logEntity.setService( + (String) ESUtil.getValueByPath(map, "fields.source" + .split("\\."))); + logEntity.setLevel( + (String) ESUtil.getValueByPath(map, "level".split("\\."))); + logEntity.setMessage( + (String) ESUtil.getValueByPath(map, "message".split("\\."))); + return logEntity; + } +} + diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/AdjacentQuery.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/AdjacentQuery.java index e9cf11e2a..9aece6b71 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/AdjacentQuery.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/AdjacentQuery.java @@ -24,7 +24,6 @@ import org.apache.hugegraph.structure.constant.Direction; import org.apache.hugegraph.structure.graph.Edge; import org.apache.hugegraph.structure.graph.Vertex; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ApplicationInfo.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ApplicationInfo.java new file mode 100644 index 000000000..23b8bafb4 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ApplicationInfo.java @@ -0,0 +1,71 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import org.apache.hugegraph.annotation.MergeProperty; +import org.apache.hugegraph.common.AppType; +import org.apache.hugegraph.common.Mergeable; +import org.apache.hugegraph.util.JsonUtil; + +@Data +@Builder +@TableName("app_info") +public class ApplicationInfo implements Mergeable { + + // graph_name in mysql {idc}-{graph_space}-{graph} (eg:bddwd-DEFAULT-graph) + @TableField(value = "graph_name") + @MergeProperty + @JsonProperty("graph_name") + private String graphName; + + @TableField(value = "app_name") + @MergeProperty + @JsonProperty("app_name") + private String appName; + + @TableField(value = "app_type") + @MergeProperty + @JsonProperty("app_type") + private AppType appType; + + @TableField(value = "count_query") + @MergeProperty + @JsonProperty("count_query") + private String countQuery; + + @TableField(value = "distribution_query") + @MergeProperty + @JsonProperty("distribution_query") + private String distributionQuery; + + @TableField(value = "description") + @MergeProperty + @JsonProperty("description") + private String description; + + @Override + public String toString() { + return JsonUtil.toJson(this); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/EgonetView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/EgonetView.java new file mode 100644 index 000000000..b1cd6fdfa --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/EgonetView.java @@ -0,0 +1,46 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EgonetView { + + @JsonProperty("json_view") + private JsonView jsonView; + + @JsonProperty("table_view") + private TableView tableView; + + @JsonProperty("graph_view") + private GraphView graphView; + + @JsonProperty("egonet") + private Set ids; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ElementEditHistory.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ElementEditHistory.java new file mode 100644 index 000000000..811fc160d --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ElementEditHistory.java @@ -0,0 +1,111 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import java.util.Date; + +import org.apache.hugegraph.annotation.MergeProperty; +import org.apache.hugegraph.common.Identifiable; +import org.apache.hugegraph.common.Mergeable; +import org.apache.hugegraph.util.JsonUtil; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@TableName("edit_history") +public class ElementEditHistory implements Identifiable, Mergeable { + + @TableId(type = IdType.AUTO) + @MergeProperty(useNew = false) + @JsonProperty("id") + private Integer id; + + @TableField(value = "graphspace") + @MergeProperty + @JsonProperty("graphspace") + private String graphspace; + + @TableField(value = "graph") + @MergeProperty + @JsonProperty("graph") + private String graph; + + @TableField(value = "element_id") + @MergeProperty + @JsonProperty("element_id") + private String elementId; + + @TableField(value = "label") + @MergeProperty + @JsonProperty("label") + private String label; + + @TableField(value = "property_num") + @MergeProperty + @JsonProperty("property_num") + private int propertyNum; + + @TableField(value = "option_type") + @MergeProperty + @JsonProperty("option_type") + private String optionType; + + @TableField(value = "option_time") + @MergeProperty + @JsonProperty("option_time") + private Date optionTime; + + @TableField(value = "option_person") + @MergeProperty + @JsonProperty("option_person") + private String optionPerson; + + @TableField(value = "content") + @MergeProperty + @JsonProperty("content") + private String content; + + public ElementEditHistory(Integer id, String graphspace, String graph, + String elementId, String label, int propertyNum, + String optionType, Date optionTime, + String optionPerson, String content) { + this.id = id; + this.graphspace = graphspace; + this.graph = graph; + this.elementId = elementId; + this.label = label; + this.propertyNum = propertyNum; + this.optionType = optionType; + this.optionTime = optionTime; + this.optionPerson = optionPerson; + this.content = content; + } + + @Override + public String toString() { + return JsonUtil.toJson(this); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ExecuteHistory.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ExecuteHistory.java index f1ddc3333..0e04d3630 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ExecuteHistory.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/ExecuteHistory.java @@ -27,7 +27,6 @@ import org.apache.hugegraph.entity.enums.ExecuteStatus; import org.apache.hugegraph.entity.enums.ExecuteType; import org.apache.hugegraph.util.SerializeUtil; - import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -35,14 +34,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor -@AllArgsConstructor @Builder @TableName("execute_history") public class ExecuteHistory implements Identifiable, Mergeable { @@ -52,10 +49,15 @@ public class ExecuteHistory implements Identifiable, Mergeable { @JsonProperty("id") private Integer id; - @TableField(value = "conn_id") + @TableField(value = "graphspace") + @MergeProperty + @JsonProperty("graphspace") + private String graphspace; + + @TableField(value = "graph") @MergeProperty - @JsonProperty("conn_id") - private Integer connId; + @JsonProperty("graph") + private String graph; @TableField(value = "async_id") @MergeProperty @@ -67,15 +69,27 @@ public class ExecuteHistory implements Identifiable, Mergeable { @JsonProperty("type") private ExecuteType type; + // gremlin/cypher @MergeProperty @JsonProperty("content") private String content; + // 用户输入的语义文本 + @TableField(value = "text") + @MergeProperty + @JsonProperty("text") + private String text; + @TableField(value = "execute_status") @MergeProperty @JsonProperty("status") private ExecuteStatus status; + @TableField(value = "failure_reason") + @MergeProperty + @JsonProperty("failure_reason") + private String failureReason; + @TableField(value = "async_status") @MergeProperty @JsonProperty("async_status") @@ -90,6 +104,44 @@ public class ExecuteHistory implements Identifiable, Mergeable { @JsonProperty("create_time") private Date createTime; + public ExecuteHistory(Integer id, String graphspace, String graph, + Long asyncId, + ExecuteType type, String content, + ExecuteStatus status, + AsyncTaskStatus asyncStatus, Long duration, + Date createTime) { + this(id, graphspace, graph, asyncId, type, content, "", status, + asyncStatus, duration, createTime); + } + + public ExecuteHistory(Integer id, String graphspace, String graph, + Long asyncId, + ExecuteType type, String content, String text, + ExecuteStatus status, AsyncTaskStatus asyncStatus, + Long duration, Date createTime) { + this(id, graphspace, graph, asyncId, type, content, text, status, + null, asyncStatus, duration, createTime); + } + + public ExecuteHistory(Integer id, String graphspace, String graph, + Long asyncId, ExecuteType type, String content, + String text, ExecuteStatus status, + String failureReason, AsyncTaskStatus asyncStatus, + Long duration, Date createTime) { + this.id = id; + this.graphspace = graphspace; + this.graph = graph; + this.asyncId = asyncId; + this.type = type; + this.content = content; + this.text = text; + this.status = status; + this.failureReason = failureReason; + this.asyncStatus = asyncStatus; + this.duration = duration; + this.createTime = createTime; + } + public void setAsyncStatus(AsyncTaskStatus status) { this.asyncStatus = status; } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/FusiformsimilarityView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/FusiformsimilarityView.java new file mode 100644 index 000000000..7d4648eaa --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/FusiformsimilarityView.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import org.apache.hugegraph.structure.traverser.FusiformSimilarity; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; +import java.util.Set; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FusiformsimilarityView { + + @JsonProperty("json_view") + private JsonView jsonView; + + @JsonProperty("table_view") + private TableView tableView; + + @JsonProperty("graph_view") + private GraphView graphView; + + @JsonProperty("similars") + private Map> similarsMap; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GraphView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GraphView.java index 4d2d8d9c6..802064db4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GraphView.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GraphView.java @@ -18,11 +18,20 @@ package org.apache.hugegraph.entity.query; +import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.hugegraph.entity.graph.VertexQueryEntity; import org.apache.hugegraph.structure.graph.Edge; import org.apache.hugegraph.structure.graph.Vertex; - +import org.apache.hugegraph.util.HubbleUtil; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -37,9 +46,175 @@ public class GraphView { public static final GraphView EMPTY = new GraphView(); + private static final int STATISTICS_ROWS = 10; + private static final int TOP_DEGREE_NUMBER = 10; @JsonProperty("vertices") - private Collection vertices; + private Collection vertices; @JsonProperty("edges") private Collection edges; + @JsonProperty("statistics") + private Map statistics = new HashMap<>(); + + public GraphView(Collection vertices, + Collection edges) { + Collection vertexQueryEntities = + new ArrayList<>(vertices.size()); + for (Vertex vertex: vertices) { + vertexQueryEntities.add(VertexQueryEntity.fromVertex(vertex)); + } + this.vertices = vertexQueryEntities; + this.edges = edges; + this.statistics = fillStatistics(vertexQueryEntities, edges); + } + + private Map fillStatistics( + Collection vertexList, + Collection edges) { + // 悬空边: 顶点展开或者图本身有悬空边 + // incidence_vertices, etc.. + Map vertices = + new HashMap<>(vertexList.size()); + for (VertexQueryEntity vertex: vertexList) { + vertices.put(vertex.id(), vertex); + } + + fillVertexStatistics(vertices, edges); + Map degrees = getDegrees(edges, vertices); + + Map statistics = new HashMap<>(STATISTICS_ROWS); + List> highestDegreeVertices = + getHighestDegreeVertices(degrees); + List isolatedVertices = getIsolatedVertices(degrees); + List> vertexLabelList = getVertexLabelList(vertices); + List isolatedEdges = getIsolatedEdges(edges, vertices); + List> edgeLabelList = getEdgeLabelList(edges); + + statistics.put("highest_degree_vertices", highestDegreeVertices); + statistics.put("isolated_vertices", isolatedVertices); + statistics.put("isolated_edges", isolatedEdges); + statistics.put("vertex_label", vertexLabelList); + statistics.put("edge_label", edgeLabelList); + return statistics; + } + + private Map getDegrees(Collection edges, + Map vertices) { + Map degrees = new HashMap<>(vertices.size()); + for (Map.Entry entry: vertices.entrySet()) { + degrees.put(entry.getKey(), 0); + } + + for (Edge edge: edges) { + // 悬空边: 会为相邻顶点计算度 + degrees.compute(edge.sourceId(), (k, v) -> v == null ? 1 : v + 1); + degrees.compute(edge.targetId(), (k, v) -> v == null ? 1 : v + 1); + } + return degrees; + } + + private void fillVertexStatistics(Map vertices, + Collection edges) { + // init + for (VertexQueryEntity vertex: vertices.values()) { + if (vertex.getStatistics().get("incidence_vertices") == null) { + Set incidenceVertices = new HashSet<>(edges.size()); + vertex.getStatistics().put("incidence_vertices", + incidenceVertices); + } + } + + // calculate + for (Edge edge: edges) { + Object source = edge.sourceId(); + Object target = edge.targetId(); + + if (vertices.get(source) == null || vertices.get(target) == null) { + continue; + } + Set incidenceVertices = HubbleUtil.uncheckedCast( + vertices.get(source).getStatistics() + .get("incidence_vertices")); + incidenceVertices.add(target); + incidenceVertices = HubbleUtil.uncheckedCast( + vertices.get(target).getStatistics() + .get("incidence_vertices")); + incidenceVertices.add(source); + } + } + + private List> getVertexLabelList(Map vertices) { + Map vertexLabels = new HashMap<>(vertices.size()); + for (Map.Entry entry: vertices.entrySet()) { + VertexQueryEntity vertex = entry.getValue(); + vertexLabels.compute(vertex.label(), (k, v) -> v == null ? 1 : v + 1); + } + return vertexLabels.entrySet().stream().map(e -> { + Map res = new HashMap<>(); + res.put("label", e.getKey()); + res.put("count", e.getValue()); + return res; + }).collect(Collectors.toList()); + } + + private List> getEdgeLabelList(Collection edges) { + Map edgeLabels = new HashMap<>(edges.size()); + for (Edge edge: edges) { + edgeLabels.compute(edge.label(), (k, v) -> v == null ? 1 : v + 1); + } + return edgeLabels.entrySet().stream().map(e -> { + Map res = new HashMap<>(); + res.put("label", e.getKey()); + res.put("count", e.getValue()); + return res; + }).collect(Collectors.toList()); + } + + private List> getHighestDegreeVertices(Map degrees) { + return degrees.entrySet().stream() + .sorted(Comparator.comparing(e -> e.getValue(), + Comparator.reverseOrder())) + .map(e -> { + Map res = new HashMap<>(); + res.put("id", e.getKey()); + res.put("degree", e.getValue()); + return res; + }).collect(Collectors.toList()) + .subList(0, Math.min(TOP_DEGREE_NUMBER, + degrees.size())); + } + + private List getIsolatedVertices(Map degrees) { + return degrees.entrySet().stream() + .filter(e -> e.getValue() == 0) + .map(e -> e.getKey()) + .collect(Collectors.toList()); + } + + private List getIsolatedEdges( + Collection edges, Map vertices) { + List isolatedEdges = new ArrayList<>(edges.size()); + for (Edge edge: edges) { + Object source = edge.sourceId(); + Object target = edge.targetId(); + + if (vertices.get(source) == null || vertices.get(target) == null) { + continue; + } + Set sourceIncidenceVertices = HubbleUtil.uncheckedCast( + vertices.get(source).getStatistics() + .get("incidence_vertices")); + Set targetIncidenceVertices = HubbleUtil.uncheckedCast( + vertices.get(target).getStatistics() + .get("incidence_vertices")); + + if (sourceIncidenceVertices.size() == 1 && + targetIncidenceVertices.size() == 1) { + isolatedEdges.add(edge.id()); + } + } + return isolatedEdges; + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinCollection.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinCollection.java index 73747c79f..9de1edae8 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinCollection.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinCollection.java @@ -23,8 +23,8 @@ import org.apache.hugegraph.annotation.MergeProperty; import org.apache.hugegraph.common.Identifiable; import org.apache.hugegraph.common.Mergeable; - import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonProperty; @@ -50,10 +50,23 @@ public class GremlinCollection implements Identifiable, Mergeable { @JsonProperty("conn_id") private Integer connId; + @MergeProperty + @JsonProperty("graphspace") + @TableField("graphspace") + private String graphSpace; + + @MergeProperty + @JsonProperty("graph") + private String graph; + @MergeProperty @JsonProperty("name") private String name; + @MergeProperty + @JsonProperty("type") + private String type; + @MergeProperty @JsonProperty("content") private String content; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinQuery.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinQuery.java index 3f6c15a1c..99b7e4499 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinQuery.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinQuery.java @@ -20,17 +20,30 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.NoArgsConstructor; @Data -@NoArgsConstructor -@AllArgsConstructor @Builder public class GremlinQuery { @JsonProperty("content") private String content; + + @JsonProperty("text") + private String text; + + public GremlinQuery() { + this.text = ""; + } + + public GremlinQuery(String content) { + this.content = content; + this.text = ""; + } + + public GremlinQuery(String content, String text) { + this.content = content; + this.text = text; + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinResult.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinResult.java index 83d43d253..96297994b 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinResult.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/GremlinResult.java @@ -19,7 +19,6 @@ package org.apache.hugegraph.entity.query; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -43,6 +42,9 @@ public class GremlinResult { @JsonProperty("graph_view") private GraphView graphView; + @JsonProperty("pathnum") + private Integer pathnum; + public enum Type { EMPTY, diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/JaccardsimilarityView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/JaccardsimilarityView.java new file mode 100644 index 000000000..20862a3e5 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/JaccardsimilarityView.java @@ -0,0 +1,35 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class JaccardsimilarityView { + + @JsonProperty("jaccardsimilarity") + private Object jaccardsimilarity; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/JsonView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/JsonView.java index 3569de53f..340297916 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/JsonView.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/JsonView.java @@ -18,15 +18,14 @@ package org.apache.hugegraph.entity.query; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + @Data @NoArgsConstructor @AllArgsConstructor diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/OlapView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/OlapView.java new file mode 100644 index 000000000..f6ce5c3a8 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/OlapView.java @@ -0,0 +1,34 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OlapView { + @JsonProperty("task_id") + private long taskId; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/RanksView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/RanksView.java new file mode 100644 index 000000000..745fc7914 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/RanksView.java @@ -0,0 +1,40 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import org.apache.hugegraph.structure.traverser.Ranks; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RanksView { + @JsonProperty("ranks") + private Ranks ranks; + + @JsonProperty("rankslist") + private List ranksList; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/SameneighborsbatchView.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/SameneighborsbatchView.java new file mode 100644 index 000000000..73578646d --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/query/SameneighborsbatchView.java @@ -0,0 +1,38 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.query; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SameneighborsbatchView { + + @JsonProperty("same_neighbors") + public List> sameNeighbors; + +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/ConflictDetail.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/ConflictDetail.java index 8886a1500..cab8fcdbe 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/ConflictDetail.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/ConflictDetail.java @@ -18,19 +18,17 @@ package org.apache.hugegraph.entity.schema; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.springframework.util.CollectionUtils; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; @Data @NoArgsConstructor @@ -64,7 +62,7 @@ public ConflictDetail(SchemaType type) { @SuppressWarnings("unchecked") public List> getConflicts( - SchemaType type) { + SchemaType type) { switch (type) { case PROPERTY_KEY: return (List>) (Object) this.pkConflicts; @@ -76,7 +74,7 @@ public List> getConflicts( return (List>) (Object) this.elConflicts; default: throw new AssertionError(String.format( - "Unknown schema type '%s'", type)); + "Unknown schema type '%s'", type)); } } @@ -109,8 +107,8 @@ public boolean anyVertexLabelConflict(Collection names) { } private boolean anyConflict( - List> conflicts, - Collection names) { + List> conflicts, + Collection names) { if (CollectionUtils.isEmpty(names)) { return false; } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelEntity.java index 4bd97090e..a10958ae9 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelEntity.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelEntity.java @@ -18,20 +18,18 @@ package org.apache.hugegraph.entity.schema; -import java.util.Date; -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.util.HubbleUtil; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableSet; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.util.HubbleUtil; + +import java.util.Date; +import java.util.List; +import java.util.Set; @Data @NoArgsConstructor @@ -42,6 +40,15 @@ public class EdgeLabelEntity implements SchemaLabelEntity, Timefiable { @JsonProperty("name") private String name; + @JsonProperty("edgelabel_type") + private String edgeLabelType; + + @JsonProperty("parent_label") + private String parentLabel; + + @JsonProperty("children") + private List children; + @JsonProperty("source_label") private String sourceLabel; @@ -61,7 +68,7 @@ public class EdgeLabelEntity implements SchemaLabelEntity, Timefiable { private List propertyIndexes; @JsonProperty("open_label_index") - private boolean openLabelIndex; + private Boolean openLabelIndex; @JsonProperty("style") private EdgeLabelStyle style; @@ -86,6 +93,8 @@ public boolean equals(Object object) { } EdgeLabelEntity other = (EdgeLabelEntity) object; return this.name.equals(other.name) && + this.edgeLabelType.equals(other.edgeLabelType) && + this.parentLabel.equals(other.parentLabel) && this.sourceLabel.equals(other.sourceLabel) && this.targetLabel.equals(other.targetLabel) && this.linkMultiTimes == other.linkMultiTimes && diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelStyle.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelStyle.java index a45951cf2..53e0215f6 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelStyle.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/EdgeLabelStyle.java @@ -18,18 +18,16 @@ package org.apache.hugegraph.entity.schema; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.util.CollectionUtils; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableList; - import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; + +import java.util.List; @Data @EqualsAndHashCode(callSuper = true) diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/LabelUpdateEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/LabelUpdateEntity.java index 78be7da66..60398bff7 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/LabelUpdateEntity.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/LabelUpdateEntity.java @@ -18,15 +18,14 @@ package org.apache.hugegraph.entity.schema; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Data; - @Data public abstract class LabelUpdateEntity implements Typifiable { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/Property.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/Property.java index c43681a54..aabdb8db3 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/Property.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/Property.java @@ -19,11 +19,12 @@ package org.apache.hugegraph.entity.schema; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.Cardinality; +import org.apache.hugegraph.structure.constant.DataType; @Data @NoArgsConstructor @@ -36,4 +37,10 @@ public class Property { @JsonProperty("nullable") private boolean nullable; + + @JsonProperty("data_type") + private DataType dataType; + + @JsonProperty("cardinality") + private Cardinality cardinality; } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyIndex.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyIndex.java index 29d798863..e5fab52cd 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyIndex.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyIndex.java @@ -18,17 +18,15 @@ package org.apache.hugegraph.entity.schema; -import java.util.List; - -import org.apache.hugegraph.structure.constant.IndexType; -import org.apache.hugegraph.util.HubbleUtil; - import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hugegraph.structure.constant.IndexType; +import org.apache.hugegraph.util.HubbleUtil; + +import java.util.List; @Data @NoArgsConstructor diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyKeyEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyKeyEntity.java index e9b6f9a1e..81b712d55 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyKeyEntity.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/PropertyKeyEntity.java @@ -22,7 +22,6 @@ import org.apache.hugegraph.structure.constant.Cardinality; import org.apache.hugegraph.structure.constant.DataType; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaConflict.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaConflict.java index ff956bf02..77210a6d4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaConflict.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaConflict.java @@ -1,22 +1,24 @@ /* + * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + * 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 org.apache.hugegraph.entity.schema; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaLabelEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaLabelEntity.java index f1b1f17d7..b40d15cc4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaLabelEntity.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaLabelEntity.java @@ -34,7 +34,7 @@ public interface SchemaLabelEntity extends SchemaEntity { List getPropertyIndexes(); - boolean isOpenLabelIndex(); + Boolean getOpenLabelIndex(); SchemaStyle getStyle(); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaStyle.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaStyle.java index 624a01ff6..027e02999 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaStyle.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaStyle.java @@ -19,5 +19,4 @@ package org.apache.hugegraph.entity.schema; public class SchemaStyle { - } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaType.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaType.java index 8e3b1c85b..669ef14bc 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaType.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/SchemaType.java @@ -69,7 +69,7 @@ public static SchemaType convert(HugeType type) { return PROPERTY_INDEX; default: throw new InternalException( - "Can't convert HugeType '%s' to SchemaType", type); + "Can't convert HugeType '%s' to SchemaType", type); } } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelEntity.java index 3edb81f97..34c6efd1f 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelEntity.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelEntity.java @@ -24,7 +24,6 @@ import org.apache.hugegraph.structure.constant.IdStrategy; import org.apache.hugegraph.util.HubbleUtil; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -54,7 +53,7 @@ public class VertexLabelEntity implements SchemaLabelEntity, Timefiable { private List propertyIndexes; @JsonProperty("open_label_index") - private boolean openLabelIndex; + private Boolean openLabelIndex; @JsonProperty("style") private VertexLabelStyle style; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelStyle.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelStyle.java index 8c8e11700..0a27a4381 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelStyle.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/VertexLabelStyle.java @@ -18,22 +18,22 @@ package org.apache.hugegraph.entity.schema; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.util.CollectionUtils; - import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableList; - import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; + +import java.util.List; @Data @EqualsAndHashCode(callSuper = true) @Builder +@JsonIgnoreProperties(ignoreUnknown = true) public class VertexLabelStyle extends SchemaStyle { @JsonProperty("icon") diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/vertexlabel/ParamEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/vertexlabel/ParamEntity.java new file mode 100644 index 000000000..ddd26963a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/vertexlabel/ParamEntity.java @@ -0,0 +1,101 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.schema.vertexlabel; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.apache.hugegraph.entity.schema.Property; +import org.apache.hugegraph.entity.schema.PropertyIndex; +import org.apache.hugegraph.entity.schema.SchemaLabelEntity; +import org.apache.hugegraph.entity.schema.SchemaType; +import org.apache.hugegraph.entity.schema.Timefiable; +import org.apache.hugegraph.structure.constant.IdStrategy; +import org.apache.hugegraph.util.HubbleUtil; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ParamEntity implements SchemaLabelEntity, Timefiable { + + @JsonProperty("name") + private String name; + + @JsonProperty("id_strategy") + private IdStrategy idStrategy; + + @JsonProperty("properties") + private Set properties; + + @JsonProperty("primary_keys") + private List primaryKeys; + + @JsonProperty("display_fields") + private List displayFields; + + @JsonProperty("property_indexes") + private List propertyIndexes; + + @JsonProperty("open_label_index") + private Boolean openLabelIndex; + + @JsonProperty("style") + private ParamStyle style; + + @JsonProperty("create_time") + private Date createTime; + + @Override + public SchemaType getSchemaType() { + return SchemaType.VERTEX_LABEL; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof ParamEntity)) { + return false; + } + ParamEntity other = (ParamEntity) object; + return this.name.equals(other.name) && + this.idStrategy == other.idStrategy && + HubbleUtil.equalCollection(this.properties, other.properties) && + HubbleUtil.equalCollection(this.primaryKeys, other.primaryKeys) && + HubbleUtil.equalCollection(this.propertyIndexes, + other.propertyIndexes) && + this.openLabelIndex == other.openLabelIndex; + } + + // @Override + public void setDisplayFields(@JsonProperty("display_fields") List displayFields) { + this.displayFields = displayFields; + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/vertexlabel/ParamStyle.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/vertexlabel/ParamStyle.java new file mode 100644 index 000000000..3c80dba22 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/schema/vertexlabel/ParamStyle.java @@ -0,0 +1,63 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.schema.vertexlabel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.entity.schema.SchemaStyle; +import org.apache.hugegraph.entity.schema.VertexLabelStyle; + +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +public class ParamStyle extends SchemaStyle { + @JsonProperty("color") + private String color; + + @JsonProperty("size") + private VertexLabelStyle.Size size; + + public ParamStyle() { + this(null, null); + } + + @JsonCreator + public ParamStyle(@JsonProperty("color") String color, + @JsonProperty("size") VertexLabelStyle.Size size) { + this.color = !StringUtils.isEmpty(color) ? color : "#5C73E6"; + this.size = size != null ? size : VertexLabelStyle.Size.NORMAL; + } + + public enum Size { + + HUGE, + + BIG, + + NORMAL, + + SMALL, + + TINY + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/BuiltInEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/BuiltInEntity.java new file mode 100644 index 000000000..8d4dc865d --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/BuiltInEntity.java @@ -0,0 +1,39 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.space; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BuiltInEntity { + @JsonProperty("init_space") + public boolean initSpace = true; + @JsonProperty("init_hlm") + public boolean initHlm = true; + @JsonProperty("init_covid19") + public boolean initCovid19 = true; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/ComputerServiceEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/ComputerServiceEntity.java new file mode 100644 index 000000000..6490c2233 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/ComputerServiceEntity.java @@ -0,0 +1,50 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.space; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ComputerServiceEntity { + @JsonProperty("id") + public long id; + @JsonProperty("task_type") + public String type; + @JsonProperty("task_name") + public String name; + @JsonProperty("task_status") + public String status; + @JsonProperty("task_progress") + public long progress; + @JsonProperty("task_algorithm") + public String algorithm; + @JsonProperty("task_description") + public String description; + @JsonProperty("task_create") + public long create; + @JsonProperty("task_input") + public String input; +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/GraphSpaceEntity.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/GraphSpaceEntity.java new file mode 100644 index 000000000..6134d11fb --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/space/GraphSpaceEntity.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.entity.space; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hugegraph.structure.space.GraphSpace; +import org.apache.hugegraph.util.JsonUtil; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class GraphSpaceEntity extends GraphSpace { + @JsonProperty("graphspace_admin") + public List graphspaceAdmin = new ArrayList<>(); + + @JsonProperty("statistic") + public Map statistic = new HashMap<>(); + + public GraphSpaceEntity() { + } + + public static GraphSpaceEntity fromGraphSpace(GraphSpace graphSpace) { + return JsonUtil.fromJson(JsonUtil.toJson(graphSpace), GraphSpaceEntity.class); + } + + public GraphSpace convertGraphSpace() { + // Generate GraphSpace instance From GraphSpaceEntity + return this; + } + + public Map getStatistic() { + return statistic; + } + + public void setStatistic(Map statistic) { + this.statistic = statistic; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTask.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTask.java index 015d2c043..f74e2de9a 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTask.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTask.java @@ -22,7 +22,6 @@ import org.apache.hugegraph.annotation.MergeProperty; import org.apache.hugegraph.util.SerializeUtil; - import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTaskResult.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTaskResult.java index 6d0dc002c..e204c357d 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTaskResult.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/task/AsyncTaskResult.java @@ -19,7 +19,6 @@ package org.apache.hugegraph.entity.task; import org.apache.hugegraph.annotation.MergeProperty; - import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/GenericException.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/GenericException.java deleted file mode 100644 index 2f41d40c2..000000000 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/GenericException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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 org.apache.hugegraph.exception; - -/** - * Used to wrap other unexpected exceptions like Client/ServerException, and convert them into this. - * This is typically done to handle exceptions uniformly in the hubble UI. - */ -public class GenericException extends ParameterizedException { - - public GenericException(ServerException e) { - super(e.getMessage(), e.getCause()); - } - - public GenericException(Exception e) { - super(e.getMessage(), e.getCause()); - } -} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/HugeException.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/HugeException.java new file mode 100644 index 000000000..f8971d490 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/HugeException.java @@ -0,0 +1,25 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.exception; + +public class HugeException extends RuntimeException { + public HugeException(String msg) { + super(msg); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/LoginThrottledException.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/LoginThrottledException.java new file mode 100644 index 000000000..6770f4ad1 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/LoginThrottledException.java @@ -0,0 +1,34 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.exception; + +import lombok.Getter; + +@Getter +public class LoginThrottledException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final long retrySeconds; + + public LoginThrottledException(long retrySeconds) { + super("auth.login.throttled"); + this.retrySeconds = retrySeconds; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/ServerCapabilityUnavailableException.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/ServerCapabilityUnavailableException.java new file mode 100644 index 000000000..77f87e7e9 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/ServerCapabilityUnavailableException.java @@ -0,0 +1,29 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.exception; + +public class ServerCapabilityUnavailableException + extends ParameterizedException { + + public ServerCapabilityUnavailableException(String message, + Throwable cause, + Object... args) { + super(message, cause, args); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/UnauthorizedException.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/UnauthorizedException.java new file mode 100644 index 000000000..2150686ef --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/exception/UnauthorizedException.java @@ -0,0 +1,25 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.exception; + +public class UnauthorizedException extends RuntimeException { + public UnauthorizedException() { + super("Unauthorized"); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomApplicationRunner.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomApplicationRunner.java index fa91feef9..d98822934 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomApplicationRunner.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomApplicationRunner.java @@ -18,6 +18,9 @@ package org.apache.hugegraph.handler; +//import org.apache.hugegraph.license.LicenseVerifier; // TODO C Remove Licence + +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.license.ServerInfo; import org.apache.hugegraph.options.HubbleOptions; @@ -26,11 +29,8 @@ import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; -import lombok.extern.log4j.Log4j2; - @Log4j2 @Component -// TODO: Why we need this class? public class CustomApplicationRunner implements ApplicationRunner { @Autowired @@ -40,5 +40,12 @@ public class CustomApplicationRunner implements ApplicationRunner { public void run(ApplicationArguments args) throws Exception { String serverId = this.config.get(HubbleOptions.SERVER_ID); ServerInfo serverInfo = new ServerInfo(serverId); + log.info("The server info has been inited"); + // this.installLicense(serverInfo, + // "9662b261c388fc5923ace0ebe2a34b02"); + } + + private void installLicense(ServerInfo serverInfo, String md5) { + //LicenseVerifier.instance().install(serverInfo, md5);// TODO C Remove Licence } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java index f7587e2a6..b48d9cdbb 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java @@ -22,40 +22,156 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; -import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.exception.InternalException; -import org.apache.hugegraph.service.license.LicenseService; +//import org.apache.hugegraph.license.LicenseVerifier; // TODO C Remove Licence +import org.apache.hugegraph.service.HugeClientPoolService; +//import org.apache.hugegraph.service.license.LicenseService;// TODO C Remove Licence import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.util.PageUtil; + import lombok.extern.log4j.Log4j2; @Log4j2 @Component public class CustomInterceptor extends HandlerInterceptorAdapter { + //@Autowired + //private LicenseService licenseService;// TODO C Remove Licence @Autowired - private LicenseService licenseService; + protected HugeClientPoolService hugeClientPoolService; private static final Pattern CHECK_API_PATTERN = - Pattern.compile(".*/graph-connections/\\d+/.+"); + Pattern.compile(".*/graph-connections/\\d+/.+"); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + validatePage(request, "page_no", false); + validatePage(request, "page_size", true); String url = request.getRequestURI(); if (!CHECK_API_PATTERN.matcher(url).matches()) { + setHugeClientToRequest(request); return true; } - String connIdValue = StringUtils.substringBetween( - url, "/graph-connections/", "/"); - if (StringUtils.isEmpty(connIdValue)) { - throw new InternalException("Not found conn id in url"); - } + // String connIdValue = StringUtils.substringBetween( + // url, "/graph-connections/", "/"); + // if (StringUtils.isEmpty(connIdValue)) { + // throw new InternalException("Not found conn id in url"); + // } + + // int connId = Integer.parseInt(connIdValue); + // Check graph connection valid + // this.licenseService.checkGraphStatus(connId); + //LicenseVerifier.instance().verifyIfNeeded(); // TODO C Remove Licence + setHugeClientToRequest(request); return true; } + + private void validatePage(HttpServletRequest request, String name, + boolean size) { + String value = request.getParameter(name); + if (!StringUtils.hasText(value)) { + return; + } + try { + int number = Integer.parseInt(value); + boolean invalid = size ? number != -1 && + (number < 1 || number > PageUtil.MAX_PAGE_SIZE) : + number < 1; + if (invalid) { + throw new ExternalException("Invalid pagination parameter: %s", name); + } + } catch (NumberFormatException e) { + throw new ExternalException("Invalid pagination parameter: %s", name); + } + } + + @Override + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, + Object handler, Exception exception) { + Object value = request.getAttribute("hugeClient"); + if (value instanceof HugeClient) { + ((HugeClient) value).close(); + request.removeAttribute("hugeClient"); + } + } + + public void setHugeClientToRequest(HttpServletRequest request) { + String uri = request.getRequestURI(); + HugeClient client = null; + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return; + } + if (this.isLoginRequest(uri)) { + client = unauthClient(); + } else { + if (this.isLogoutRequest(uri)) { + return; + } + if (!this.hasAuthSession(request)) { + return; + } + String token = + (String) request.getSession().getAttribute(Constant.TOKEN_KEY); + String [] res = uri.split("/"); + String graphSpace = null; + String graph = null; + for (int i = 0; i < res.length; i++) { + if ("graphspaces".equals(res[i]) && i < res.length - 1) { + graphSpace = res[i + 1]; + } + if ("graphs".equals(res[i]) && i < res.length - 1) { + graph = res[i + 1]; + } + } + client = this.authClient(graphSpace, graph, token); + } + + request.setAttribute("hugeClient", client); + } + + private boolean isLoginRequest(String uri) { + return (Constant.API_VERSION + "auth/login").equals(uri) || + uri.endsWith("/auth/login"); + } + + private boolean isLogoutRequest(String uri) { + return (Constant.API_VERSION + "auth/logout").equals(uri) || + uri.endsWith("/auth/logout"); + } + + private boolean hasAuthSession(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return false; + } + return this.hasTextSessionAttribute(session, Constant.TOKEN_KEY) && + this.hasTextSessionAttribute(session, Constant.USERNAME_KEY); + } + + private boolean hasTextSessionAttribute(HttpSession session, String key) { + Object value = session.getAttribute(key); + return value instanceof String && StringUtils.hasText((String) value); + } + + protected HugeClient authClient(String graphSpace, String graph, + String token) { + return this.hugeClientPoolService.createAuthClient(graphSpace, graph, + token); + } + + protected HugeClient unauthClient() { + return this.hugeClientPoolService.createUnauthClient(); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/EdgeMappingTypeHandler.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/EdgeMappingTypeHandler.java index fbeb04e92..cdac5b206 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/EdgeMappingTypeHandler.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/EdgeMappingTypeHandler.java @@ -25,12 +25,13 @@ import java.util.Set; import org.apache.hugegraph.entity.load.EdgeMapping; -import org.apache.hugegraph.loader.util.JsonUtil; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; +import org.apache.hugegraph.loader.util.JsonUtil; + @MappedTypes(value = {EdgeMapping.class}) @MappedJdbcTypes(value = {JdbcType.VARCHAR}) public class EdgeMappingTypeHandler extends BaseTypeHandler> { @@ -45,7 +46,7 @@ public void setNonNullParameter(PreparedStatement preparedStatement, @Override public Set getNullableResult(ResultSet resultSet, String columnName) - throws SQLException { + throws SQLException { String json = resultSet.getString(columnName); return JsonUtil.convertSet(json, EdgeMapping.class); } @@ -53,15 +54,15 @@ public Set getNullableResult(ResultSet resultSet, @Override public Set getNullableResult(ResultSet resultSet, int columnIndex) - throws SQLException { + throws SQLException { String json = resultSet.getString(columnIndex); return JsonUtil.convertSet(json, EdgeMapping.class); } @Override public Set getNullableResult( - CallableStatement callableStatement, - int columnIndex) throws SQLException { + CallableStatement callableStatement, + int columnIndex) throws SQLException { String json = callableStatement.getString(columnIndex); return JsonUtil.convertSet(json, EdgeMapping.class); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ErrorCodeMessage.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ErrorCodeMessage.java new file mode 100644 index 000000000..43c32a7a2 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ErrorCodeMessage.java @@ -0,0 +1,193 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.handler; + +import java.util.List; + +public class ErrorCodeMessage { + public static String getErrorMessage(String code, String message, + Object... args) { + // 1: server 2:client + if (code.startsWith("1")) { + return ServerCode.fromCode(code, message, args); + } else if (code.startsWith("2")) { + return ClientCode.fromCode(code, message, args); + } else { + return String.format("无效的错误码: %s,错误信息:%s", code, message); + } + } + + public enum ClientCode { + + // 00-xxx for common error + SUCCEED("200-000", "执行成功"), + + FAILED("200-001", "%s"), + + NULL_VALUE("200-002", "参数 %s 不能为 null"), + + INVALID_VALUE("200-003", "%s!"), + + MISSING_PARAM("200-004", "缺少参数 %s"), + + NOT_EXISTS("200-005", "参数 %s 不存在!"), + + EXISTS("200-006", "%s 已存在!"), + + INVALID_PARAM("200-007", "%s(%s)取值非法!"), + + INVALID_VALUE_OUT_RANGE("200-008", + "参数越界:参数 %s 的取值范围为 %s."), + + // 01-xxx for graphSpace error + // 02-xxx for graphs error + // 03-xxx for schema error + UNDEFINED_PROPERTY_KEY("203-000", "不存在的属性类型:%s"), + + UNDEFINED_VERTEX_LABEL("203-001", "不存在的顶点类型:%s"), + + UNDEFINED_EDGE_LABEL("203-002", "不存在的边类型:%s"), + // 04-xxx for vertex and edge error + // 05-xxx for oltp algorithms error + + /* + * param out of range error + * param name: degree, capacity, limit + * */ + PARAM_OUT_RANGE("205-000", "%s 取值范围 > 0 或 == %s,当前值:%s"), + + PARAM_GREATER("205-001", "%s 必须 >= %s,当前值为 '%s' 和 '%s'"), + + PARAM_SMALLER("205-002", "%s 必须 < %s"), + ; + + private String code; + private String message; + private List attach; + + ClientCode(String code, String message) { + this.code = code; + this.message = message; + } + + public static String fromCode(String code, String message, + Object... args) { + for (ClientCode cc: ClientCode.values()) { + if (cc.code.equals(code)) { + return cc.getMessage(args); + } + } + return String.format("无效的错误码: %s,错误信息:%s", code, message); + } + + public String getMessage(Object... args) { + return String.format(this.message, args); + } + } + + public enum ServerCode { + + // 00-xxx for common error + SUCCEED("100-000", "执行成功"), + + FAILED("100-001", "%s"), + + NULL_VALUE("100-002", "参数 %s 不能为 null"), + + INVALID_VALUE("100-003", "%s!"), + + MISSING_PARAM("100-004", "缺少参数 %s"), + + NOT_EXISTS("100-005", "参数 %s 不存在!"), + + EXISTS("100-006", "%s 已存在!"), + + INVALID_PARAM("100-007", "%s(%s)取值非法!"), + + INVALID_VALUE_OUT_RANGE("100-008", + "参数越界:参数 %s 的取值范围为 %s."), + // 01-xxx for graphSpace error + // 02-xxx for graphs error + // 03-xxx for schema error + UNDEFINED_PROPERTY_KEY("103-000", "不存在的属性类型:%s"), + + UNDEFINED_VERTEX_LABEL("103-001", "不存在的顶点类型 %s"), + + UNDEFINED_EDGE_LABEL("103-002", "不存在的边类型:%s"), + + INVALID_PROP_VALUE("103-003", "属性值 %s 的数据类型错误, " + + "需要的类型(字段)为 %s(%s), 实际类型 %s"), + + // 04-xxx for vertex and edge error + VERTEX_NOT_EXIST("104-001", "顶点 %s 不存在"), + + VERTEX_NAME_NOT_EXIST("104-002", "参数 %s 的值 '%s' 不存在"), + + IDS_NOT_EXIST("104-003", "不存在的 ids %s"), + + EDGE_INVALID_LINK("104-004", "不存在的 source/target 类型 %s,%s 对应边类型 %s"), + + LABEL_PROP_NOT_EXIST("104-005", "不存在顶点类型为 %s,属性为 %s 的顶点"), + + // 05-xxx for oltp algorithms error + REACH_CAPACITY("105-003", "达到临界 capacity %s"), + + REACH_CAPACITY_WITH_DEPTH("105-004", "达到临界 capacity %s," + + "剩余 depth 为 %s"), + + EXCEED_CAPACITY_WHILE_FINDING("105-005", "达到临界 capacity %s," + + "查找对象:%s"), + + INVALID_LIMIT("105-006", "无效的 limit %s, 必须 <= capacity(%s)"), + + PARAM_GREATER("105-007", "%s 必须 >= %s, 当前值为 %s 和 %s"), + + PARAM_SMALLER("105-008", "%s 必须 < %s"), + + DEPTH_OUT_RANGE("105-009", "depth 取值范围 (0, 5000], " + + "当前值: %s"), + + VERTEX_PAIR_LENGTH_ERROR("105-010", "顶点对长度错误"), + + SOURCE_TARGET_SAME("105-011", "source 和 target 不能取相同 id"), + ; + + private String code; + private String message; + + ServerCode(String code, String message) { + this.code = code; + this.message = message; + } + + public static String fromCode(String code, String message, + Object... args) { + for (ServerCode sc: ServerCode.values()) { + if (sc.code.equals(code)) { + return sc.getMessage(args); + } + } + return String.format("无效的错误码: %s,错误信息:%s", code, message); + } + + public String getMessage(Object... args) { + return String.format(this.message, args); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java index 5ddefbb19..ab75117d0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java @@ -18,21 +18,37 @@ package org.apache.hugegraph.handler; -import org.apache.hugegraph.common.Constant; -import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ServerException; import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.exception.GenericException; import org.apache.hugegraph.exception.IllegalGremlinException; import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.exception.LoginThrottledException; import org.apache.hugegraph.exception.ParameterizedException; +import org.apache.hugegraph.exception.ServerCapabilityUnavailableException; +import org.apache.hugegraph.exception.UnauthorizedException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.hugegraph.util.HubbleUtil; import lombok.extern.log4j.Log4j2; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + @Log4j2 @RestControllerAdvice public class ExceptionAdvisor { @@ -40,78 +56,158 @@ public class ExceptionAdvisor { @Autowired private MessageSourceHandler messageSourceHandler; + @ExceptionHandler(LoginThrottledException.class) + public ResponseEntity exceptionHandler(LoginThrottledException e) { + String message = this.handleMessage(e.getMessage(), + new Object[]{e.getRetrySeconds()}); + Response response = Response.builder() + .status(HttpStatus.TOO_MANY_REQUESTS.value()) + .message(message) + .cause(null) + .build(); + return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) + .header("Retry-After", + String.valueOf(e.getRetrySeconds())) + .body(response); + } + + // FIXME: Map business failures to HTTP status codes only after auditing frontend + // compatibility; clients currently depend on the status field in the JSON body. + @ExceptionHandler(InternalException.class) @ResponseStatus(HttpStatus.OK) public Response exceptionHandler(InternalException e) { - log.error("InternalException:", e); + log.warn("Internal request failure"); String message = this.handleMessage(e.getMessage(), e.args()); + closeRequestClient(); return Response.builder() .status(Constant.STATUS_INTERNAL_ERROR) .message(message) - .cause(e.getCause()) + .cause(null) .build(); } @ExceptionHandler(ExternalException.class) @ResponseStatus(HttpStatus.OK) public Response exceptionHandler(ExternalException e) { - log.error("ExternalException:", e); + log.debug("External request failure: {}", e.getMessage()); String message = this.handleMessage(e.getMessage(), e.args()); + closeRequestClient(); return Response.builder() .status(e.status()) .message(message) - .cause(e.getCause()) + .cause(null) .build(); } - @ExceptionHandler(GenericException.class) + @ExceptionHandler(ParameterizedException.class) @ResponseStatus(HttpStatus.OK) - public Response exceptionHandler(GenericException e) { - log.error("GenericException:", e); + public Response exceptionHandler(ParameterizedException e) { + String message = this.handleMessage(e.getMessage(), e.args()); + closeRequestClient(); return Response.builder() .status(Constant.STATUS_BAD_REQUEST) - .message("Faied to connect the graph server. Please refer to the " + - "Hubble log for details.") + .message(message) .cause(null) .build(); } - @ExceptionHandler(ParameterizedException.class) + @ExceptionHandler(ServerException.class) @ResponseStatus(HttpStatus.OK) - public Response exceptionHandler(ParameterizedException e) { - log.error("ParameterizedException", e); - String message = this.handleMessage(e.getMessage(), e.args()); + public Response exceptionHandler(ServerException e) { + log.warn("HugeGraph Server request failed"); + + String message = this.handleMessage(e.getMessage(), null); + closeRequestClient(); return Response.builder() .status(Constant.STATUS_BAD_REQUEST) .message(message) - .cause(e.getCause()) + .cause(null) .build(); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.OK) public Response exceptionHandler(Exception e) { - log.error("Exception:", e); + log.error("Unexpected request failure", e); String message = this.handleMessage(e.getMessage(), null); + closeRequestClient(); return Response.builder() .status(Constant.STATUS_BAD_REQUEST) .message(message) - .cause(e.getCause()) + .cause(null) + .build(); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Response exceptionHandler( + MissingServletRequestParameterException e) { + String message = this.handleMessage("request.parameter.required", + new Object[]{e.getParameterName()}); + closeRequestClient(); + return Response.builder() + .status(Constant.STATUS_BAD_REQUEST) + .message(message) + .cause(null) + .build(); + } + + @ExceptionHandler(ServerCapabilityUnavailableException.class) + @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + public Response exceptionHandler( + ServerCapabilityUnavailableException e) { + log.warn("Required HugeGraph Server capability is unavailable", e); + String message = this.handleMessage(e.getMessage(), e.args()); + closeRequestClient(); + return Response.builder() + .status(HttpStatus.SERVICE_UNAVAILABLE.value()) + .message(message) + .cause(null) .build(); } @ExceptionHandler(IllegalGremlinException.class) @ResponseStatus(HttpStatus.OK) public Response exceptionHandler(IllegalGremlinException e) { - log.error("IllegalGremlinException:", e); + log.debug("Illegal Gremlin request: {}", e.getMessage()); String message = this.handleMessage(e.getMessage(), e.args()); + closeRequestClient(); return Response.builder() .status(Constant.STATUS_ILLEGAL_GREMLIN) .message(message) - .cause(e.getCause()) + .cause(null) .build(); } + @ExceptionHandler(UnauthorizedException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Response exceptionHandler(UnauthorizedException e) { + log.debug("Unauthorized request: {}", e.getMessage()); + String message = e.getMessage(); + closeRequestClient(); + return Response.builder() + .status(Constant.STATUS_UNAUTHORIZED) + .message(message) + .cause(null) + .build(); + } + + protected HttpServletRequest getRequest() { + return ((ServletRequestAttributes) + RequestContextHolder.getRequestAttributes()).getRequest(); + } + + public void closeRequestClient() { + HttpServletRequest httpRequest = getRequest(); + if (httpRequest.getAttribute("hugeClient") != null) { + HugeClient client = (HugeClient) httpRequest.getAttribute( + "hugeClient"); + client.close(); + httpRequest.removeAttribute("hugeClient"); + } + } + private String handleMessage(String message, Object[] args) { String[] strArgs = null; if (args != null && args.length > 0) { @@ -120,6 +216,8 @@ private String handleMessage(String message, Object[] args) { strArgs[i] = args[i] != null ? args[i].toString() : "?"; } } + message = handleErrorCode(message); + try { message = this.messageSourceHandler.getMessage(message, strArgs); } catch (Throwable e) { @@ -127,4 +225,28 @@ private String handleMessage(String message, Object[] args) { } return message; } + + private String handleErrorCode(String message) { + // message with ErrorCode is Json String + if (message != null && message.startsWith("{\"")) { + try { + Map result = HubbleUtil.uncheckedCast( + JsonUtil.fromJson(message, Map.class)); + if (result.containsKey("code") && result.containsKey("message")) { + String code = result.get("code").toString(); + String origin = result.get("message").toString(); + List attach = + HubbleUtil.uncheckedCast(result.get("attach")); + message = ErrorCodeMessage.getErrorMessage(code, origin, + attach.toArray()); + } + } catch (Exception e) { + throw new RuntimeException( + String.format("Fail to handle error code for message " + + "%s, error: %s", message, + e.getMessage())); + } + } + return message; + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/HubbleDisposableBean.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/HubbleDisposableBean.java index 54c216963..38ebdd834 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/HubbleDisposableBean.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/HubbleDisposableBean.java @@ -18,13 +18,12 @@ package org.apache.hugegraph.handler; +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.service.load.LoadTaskService; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import lombok.extern.log4j.Log4j2; - @Log4j2 @Component public class HubbleDisposableBean implements DisposableBean { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoadTaskExecutor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoadTaskExecutor.java index d660c273d..07e7ca164 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoadTaskExecutor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoadTaskExecutor.java @@ -31,8 +31,13 @@ public class LoadTaskExecutor { @Async public void execute(LoadTask task, Runnable callback) { log.info("Executing task: {}", task.getId()); - task.run(); - log.info("Executed task: {}, update status to db", task.getId()); - callback.run(); + try { + task.run(); + } catch (Throwable t) { + log.warn("Executing task: {} error", task.getId(), t); + } finally { + log.info("Executed task: {}, update status to db", task.getId()); + callback.run(); + } } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java new file mode 100644 index 000000000..b0c1dd1ed --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java @@ -0,0 +1,52 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.handler; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.exception.UnauthorizedException; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class LoginInterceptor extends HandlerInterceptorAdapter { + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) { + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + + if (!this.hasTextSessionAttribute(request, Constant.TOKEN_KEY) || + !this.hasTextSessionAttribute(request, Constant.USERNAME_KEY)) { + throw new UnauthorizedException(); + } + + return true; + } + + private boolean hasTextSessionAttribute(HttpServletRequest request, + String key) { + Object value = request.getSession().getAttribute(key); + return value instanceof String && StringUtils.hasText((String) value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java index 62c1119d8..8e49bd560 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java @@ -18,12 +18,9 @@ package org.apache.hugegraph.handler; -import java.util.Locale; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; - +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.LocaleUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.entity.UserInfo; import org.apache.hugegraph.service.UserInfoService; @@ -36,7 +33,9 @@ import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.util.WebUtils; -import lombok.extern.log4j.Log4j2; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Locale; @Log4j2 @Component @@ -68,6 +67,10 @@ private Locale getLocale() { return DEFAULT_LOCALE; } + if (StringUtils.isNotBlank(this.request.getHeader("Accept-Language"))) { + return this.request.getLocale(); + } + UserInfo userInfo = this.getUserInfo(); if (userInfo != null && userInfo.getLocale() != null) { return LocaleUtils.toLocale(userInfo.getLocale()); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java index b0c16bf4b..a7befeb92 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java @@ -18,23 +18,31 @@ package org.apache.hugegraph.handler; -import org.apache.hugegraph.common.Response; +import javax.servlet.http.HttpServletRequest; + import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; +import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.driver.HugeClient; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 @RestControllerAdvice(basePackages = "org.apache.hugegraph.controller") public class ResponseAdvisor implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class> - converterType) { + converterType) { return true; } @@ -42,9 +50,10 @@ public boolean supports(MethodParameter returnType, public Response beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> - selectedConverterType, + selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { + closeRequestClient(request); if (body instanceof Response) { // The exception response return (Response) body; @@ -52,6 +61,18 @@ public Response beforeBodyWrite(Object body, MethodParameter returnType, return Response.builder() .status(HttpStatus.OK.value()) .data(body) + .message("Success") .build(); } + + public void closeRequestClient(ServerHttpRequest request) { + HttpServletRequest httpRequest = + ((ServletServerHttpRequest) request).getServletRequest(); + if (httpRequest.getAttribute("hugeClient") != null) { + HugeClient client = (HugeClient) httpRequest.getAttribute( + "hugeClient"); + client.close(); + httpRequest.removeAttribute("hugeClient"); + } + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/VertexMappingTypeHandler.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/VertexMappingTypeHandler.java index a0018e884..391d40135 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/VertexMappingTypeHandler.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/VertexMappingTypeHandler.java @@ -25,16 +25,17 @@ import java.util.Set; import org.apache.hugegraph.entity.load.VertexMapping; -import org.apache.hugegraph.loader.util.JsonUtil; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; +import org.apache.hugegraph.loader.util.JsonUtil; + @MappedTypes(value = {VertexMapping.class}) @MappedJdbcTypes(value = {JdbcType.VARCHAR}) public class VertexMappingTypeHandler - extends BaseTypeHandler> { + extends BaseTypeHandler> { @Override public void setNonNullParameter(PreparedStatement preparedStatement, @@ -47,7 +48,7 @@ public void setNonNullParameter(PreparedStatement preparedStatement, @Override public Set getNullableResult(ResultSet resultSet, String columnName) - throws SQLException { + throws SQLException { String json = resultSet.getString(columnName); return JsonUtil.convertSet(json, VertexMapping.class); } @@ -55,15 +56,15 @@ public Set getNullableResult(ResultSet resultSet, @Override public Set getNullableResult(ResultSet resultSet, int columnIndex) - throws SQLException { + throws SQLException { String json = resultSet.getString(columnIndex); return JsonUtil.convertSet(json, VertexMapping.class); } @Override public Set getNullableResult( - CallableStatement callableStatement, - int columnIndex) throws SQLException { + CallableStatement callableStatement, + int columnIndex) throws SQLException { String json = callableStatement.getString(columnIndex); return JsonUtil.convertSet(json, VertexMapping.class); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/ExtraParam.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/ExtraParam.java new file mode 100644 index 000000000..4095f6513 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/ExtraParam.java @@ -0,0 +1,132 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.license; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ExtraParam { + + @JsonProperty("username") + private String username; + + @JsonProperty("license_type") + private String licenseType; + + @JsonProperty("id") + private String id; + + @JsonProperty("version") + private String version; + + @JsonProperty("graphs") + private int graphs; + + @JsonProperty("ip") + private String ip; + + @JsonProperty("mac") + private String mac; + + @JsonProperty("cpus") + private int cpus; + + // The unit is MB + @JsonProperty("ram") + private int ram; + + @JsonProperty("threads") + private int threads; + + // The unit is MB + @JsonProperty("memory") + private int memory; + + @JsonProperty("nodes") + private int nodes; + + // The unit is MB + @JsonProperty("data_size") + private long dataSize; + + @JsonProperty("vertices") + private long vertices; + + @JsonProperty("edges") + private long edges; + + public String username() { + return this.username; + } + + public String licenseType() { + return this.licenseType; + } + + public String id() { + return this.id; + } + + public String version() { + return this.version; + } + + public int graphs() { + return this.graphs; + } + + public String ip() { + return this.ip; + } + + public String mac() { + return this.mac; + } + + public int cpus() { + return this.cpus; + } + + public int ram() { + return this.ram; + } + + public int threads() { + return this.threads; + } + + public int memory() { + return this.memory; + } + + public int nodes() { + return this.nodes; + } + + public long dataSize() { + return this.dataSize; + } + + public long vertices() { + return this.vertices; + } + + public long edges() { + return this.edges; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/LicenseVerifyParam.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/LicenseVerifyParam.java new file mode 100644 index 000000000..dd109e45c --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/LicenseVerifyParam.java @@ -0,0 +1,61 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.license; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class LicenseVerifyParam { + + @JsonProperty("subject") + private String subject; + + @JsonProperty("public_alias") + private String publicAlias; + + @JsonAlias("store_ticket") + @JsonProperty("store_password") + private String storePassword; + + @JsonProperty("publickey_path") + private String publicKeyPath; + + @JsonProperty("license_path") + private String licensePath; + + public String subject() { + return this.subject; + } + + public String publicAlias() { + return this.publicAlias; + } + + public String storePassword() { + return this.storePassword; + } + + public String licensePath() { + return this.licensePath; + } + + public String publicKeyPath() { + return this.publicKeyPath; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/MachineInfo.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/MachineInfo.java new file mode 100644 index 000000000..a0fd29e50 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/license/MachineInfo.java @@ -0,0 +1,127 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.license; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class MachineInfo { + + private List ipAddressList; + private List macAddressList; + + public MachineInfo() { + this.ipAddressList = null; + this.macAddressList = null; + } + + public List getIpAddress() { + if (this.ipAddressList != null) { + return this.ipAddressList; + } + this.ipAddressList = new ArrayList<>(); + List inetAddresses = this.getLocalAllInetAddress(); + if (inetAddresses != null && !inetAddresses.isEmpty()) { + this.ipAddressList = inetAddresses.stream() + .map(InetAddress::getHostAddress) + .distinct() + .map(String::toLowerCase) + .collect(Collectors.toList()); + } + return this.ipAddressList; + } + + public List getMacAddress() { + if (this.macAddressList != null) { + return this.macAddressList; + } + this.macAddressList = new ArrayList<>(); + List inetAddresses = this.getLocalAllInetAddress(); + if (inetAddresses != null && !inetAddresses.isEmpty()) { + // Get the Mac address of all network interfaces + List list = new ArrayList<>(); + Set uniqueValues = new HashSet<>(); + for (InetAddress inetAddress : inetAddresses) { + String macByInetAddress = this.getMacByInetAddress(inetAddress); + if (uniqueValues.add(macByInetAddress)) { + list.add(macByInetAddress); + } + } + this.macAddressList = list; + } + return this.macAddressList; + } + + public List getLocalAllInetAddress() { + Enumeration interfaces; + try { + interfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + throw new RuntimeException("Failed to get network interfaces"); + } + + List result = new ArrayList<>(); + while (interfaces.hasMoreElements()) { + NetworkInterface nw = interfaces.nextElement(); + for (Enumeration inetAddresses = nw.getInetAddresses(); + inetAddresses.hasMoreElements(); ) { + InetAddress inetAddr = inetAddresses.nextElement(); + if (!inetAddr.isLoopbackAddress() && + !inetAddr.isLinkLocalAddress() && + !inetAddr.isMulticastAddress()) { + result.add(inetAddr); + } + } + } + return result; + } + + public String getMacByInetAddress(InetAddress inetAddr) { + byte[] mac; + try { + mac = NetworkInterface.getByInetAddress(inetAddr) + .getHardwareAddress(); + } catch (Exception e) { + throw new RuntimeException(String.format( + "Failed to get mac address for inet address '%s'", + inetAddr)); + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mac.length; i++) { + if (i != 0) { + sb.append("-"); + } + String temp = Integer.toHexString(mac[i] & 0xff); + if (temp.length() == 1) { + sb.append("0").append(temp); + } else { + sb.append(temp); + } + } + return sb.toString().toUpperCase(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/GraphConnectionMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/GraphConnectionMapper.java deleted file mode 100644 index d65cc3f6d..000000000 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/GraphConnectionMapper.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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 org.apache.hugegraph.mapper; - -import org.apache.hugegraph.entity.GraphConnection; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; -import org.springframework.stereotype.Component; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.baomidou.mybatisplus.core.metadata.IPage; - -@Mapper -@Component -public interface GraphConnectionMapper extends BaseMapper { - - /** - * NOTE: Page must be the first param, otherwise throw exception - */ - @Select("SELECT *, " + - "(CASE WHEN `name` LIKE concat('%', #{content}, '%') AND " + - " `graph` LIKE concat('%', #{content}, '%') THEN 0 " + - " WHEN `name` LIKE concat('%', #{content}, '%') THEN 1 " + - " WHEN `graph` LIKE concat('%', #{content}, '%') THEN 2 " + - "END) as relation_sort " + - "FROM `graph_connection` " + - "WHERE `name` LIKE concat('%', #{content}, '%') OR " + - "`graph` LIKE concat('%', #{content}, '%') " + - "ORDER BY relation_sort ASC, `create_time` DESC") - IPage selectByContentInPage(IPage page, - @Param("content") - String content); -} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/UserInfoMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/UserInfoMapper.java index 9c1f9134a..a663df957 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/UserInfoMapper.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/UserInfoMapper.java @@ -18,14 +18,12 @@ package org.apache.hugegraph.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.hugegraph.entity.UserInfo; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Component; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; - @Mapper @Component public interface UserInfoMapper extends BaseMapper { - } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/algorithm/AsyncTaskMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/algorithm/AsyncTaskMapper.java index 9f641e746..5954a243c 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/algorithm/AsyncTaskMapper.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/algorithm/AsyncTaskMapper.java @@ -18,14 +18,12 @@ package org.apache.hugegraph.mapper.algorithm; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.hugegraph.entity.task.AsyncTask; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Component; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; - @Mapper @Component public interface AsyncTaskMapper extends BaseMapper { - } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/DatasourceMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/DatasourceMapper.java new file mode 100644 index 000000000..606fbce5d --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/DatasourceMapper.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.mapper.load; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.hugegraph.entity.load.Datasource; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DatasourceMapper extends BaseMapper { +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/FileMappingMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/FileMappingMapper.java index 356122cd9..bfb4cfd87 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/FileMappingMapper.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/FileMappingMapper.java @@ -18,14 +18,13 @@ package org.apache.hugegraph.mapper.load; -import org.apache.hugegraph.entity.load.FileMapping; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Component; +import org.apache.hugegraph.entity.load.FileMapping; import com.baomidou.mybatisplus.core.mapper.BaseMapper; @Mapper @Component public interface FileMappingMapper extends BaseMapper { - } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/JobManagerMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/JobManagerMapper.java index 310b16b68..f1eb255b5 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/JobManagerMapper.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/JobManagerMapper.java @@ -18,13 +18,13 @@ package org.apache.hugegraph.mapper.load; -import org.apache.hugegraph.entity.load.JobManager; -import org.apache.hugegraph.entity.load.JobManagerItem; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Component; +import org.apache.hugegraph.entity.load.JobManager; +import org.apache.hugegraph.entity.load.JobManagerItem; import com.baomidou.mybatisplus.core.mapper.BaseMapper; @Mapper diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/LoadTaskMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/LoadTaskMapper.java index e007d0e16..ef5ca5704 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/LoadTaskMapper.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/load/LoadTaskMapper.java @@ -18,14 +18,12 @@ package org.apache.hugegraph.mapper.load; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.hugegraph.entity.load.LoadTask; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Component; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; - @Mapper @Component public interface LoadTaskMapper extends BaseMapper { - } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/ApplicationInfoMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/ApplicationInfoMapper.java new file mode 100644 index 000000000..808751c9c --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/ApplicationInfoMapper.java @@ -0,0 +1,69 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.mapper.query; + +import java.util.List; + +import org.apache.hugegraph.entity.query.ApplicationInfo; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface ApplicationInfoMapper extends BaseMapper { + + @Select("SELECT COUNT(*) FROM `app_info` WHERE graph_name = #{graphName} AND " + + "app_name = #{appName} AND app_type = #{appType}") + int countByAppNameAndAppType(@Param("graphName") String graphName, + @Param("appName") String appName, + @Param("appType") String appType); + + @Insert("INSERT INTO app_info (graph_name, app_name, app_type, " + + "count_query, distribution_query) VALUES (#{graphName}, " + + "#{appName}, #{appType}, #{countQuery}, #{distributionQuery})") + int insertAppInfo(ApplicationInfo appInfo); + + @Update("UPDATE `app_info` SET graph_name = #{graphName}, " + + "count_query = #{countQuery}, distribution_query = #{distributionQuery} " + + "WHERE graph_name = #{graphName} AND app_name = #{appName} AND app_type = #{appType}") + int updateAppInfo(ApplicationInfo appInfo); + + @Select("SELECT * FROM `app_info` WHERE graph_name = #{graphName}") + List queryByGraph(@Param("graphName") String graphName); + + @Select("SELECT * FROM `app_info` WHERE graph_name = #{graphName} AND " + + "app_name = #{appName} AND app_type = #{appType}") + List query(@Param("graphName") String graphName, + @Param("appName") String appName, + @Param("appType") String appType); + + // 方法用于插入或更新数据 + default int insertOrUpdateAppInfo(ApplicationInfo appInfo) { + int count = countByAppNameAndAppType(appInfo.getGraphName(), + appInfo.getAppName(), + appInfo.getAppType().string()); + if (count > 0) { + return updateAppInfo(appInfo); + } else { + return insertAppInfo(appInfo); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/EditElementHistoryMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/EditElementHistoryMapper.java new file mode 100644 index 000000000..53badfe49 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/EditElementHistoryMapper.java @@ -0,0 +1,71 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.mapper.query; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.hugegraph.entity.query.ElementEditHistory; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Mapper +@Component +public interface EditElementHistoryMapper extends + BaseMapper { + @Select("SELECT * FROM `edit_history` LIMIT #{limit}") + List queryByLimit(@Param("limit") int limit); + + @Select("SELECT * FROM `edit_history` WHERE graphspace = #{graphspace} " + + "AND graph = #{graph} AND element_id = #{elementId}") + List queryByElementId( + @Param("graphspace") String graphspace, + @Param("graph") String graph, + @Param("elementId") String elementId); + + @Select("") + List queryByElementIds( + @Param("graphspace") String graphspace, + @Param("graph") String graph, + @Param("elementIds") List elementIds); + + // 批量插入方法 + @Insert({ + "" + }) + int insertBatch(@Param("list") List list); +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/ExecuteHistoryMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/ExecuteHistoryMapper.java index 71f6520d9..4d62ad362 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/ExecuteHistoryMapper.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/ExecuteHistoryMapper.java @@ -18,20 +18,30 @@ package org.apache.hugegraph.mapper.query; -import org.apache.hugegraph.entity.query.ExecuteHistory; -import org.apache.ibatis.annotations.Delete; +import java.util.List; + import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Component; +import org.apache.hugegraph.entity.query.ExecuteHistory; import com.baomidou.mybatisplus.core.mapper.BaseMapper; @Mapper @Component public interface ExecuteHistoryMapper extends BaseMapper { - @Delete("DELETE FROM `execute_history` WHERE `id` IN (" + - "SELECT `id` FROM `execute_history` ORDER BY `create_time` DESC " + - "LIMIT #{limit} OFFSET #{limit})") - void deleteExceedLimit(@Param("limit") int limit); + // 删除超出限制的记录 + default void deleteExceedLimit(int limit) { + List ids = findIdsToDelete(limit, limit); + if (!ids.isEmpty()) { + deleteBatchIds(ids); + } + } + + // 查询满足条件的 id 列表 + @Select("SELECT id FROM `execute_history` ORDER BY `create_time` DESC " + + "LIMIT #{limit} OFFSET #{offset}") + List findIdsToDelete(@Param("limit") int limit, @Param("offset") int offset); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/GremlinCollectionMapper.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/GremlinCollectionMapper.java index 06f38e2d9..0a92eb199 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/GremlinCollectionMapper.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/mapper/query/GremlinCollectionMapper.java @@ -18,12 +18,12 @@ package org.apache.hugegraph.mapper.query; -import org.apache.hugegraph.entity.query.GremlinCollection; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Component; +import org.apache.hugegraph.entity.query.GremlinCollection; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; @@ -38,12 +38,14 @@ public interface GremlinCollectionMapper extends BaseMapper { " WHEN `content` LIKE concat('%', #{content}, '%') THEN 2 " + "END) as relation_sort " + "FROM `gremlin_collection` " + - "WHERE `conn_id` = #{conn_id} AND " + + "WHERE `graphspace` = #{graphspace} AND " + + "`graph` = #{graph} AND `type` = #{type} AND" + "(`name` LIKE concat('%', #{content}, '%') OR " + "`content` LIKE concat('%', #{content}, '%')) " + "ORDER BY relation_sort ASC, `create_time` DESC") IPage selectByContentInPage(IPage page, - @Param("conn_id") int connId, - @Param("content") - String content); + @Param("graphspace") String graphSpace, + @Param("graph") String graph, + @Param("content") String content, + @Param("type") String type); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java index b88ec2d3c..0522ac909 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java @@ -23,12 +23,13 @@ import static org.apache.hugegraph.config.OptionChecker.positiveInt; import static org.apache.hugegraph.config.OptionChecker.rangeInt; +import org.springframework.util.CollectionUtils; + import org.apache.hugegraph.config.ConfigListOption; import org.apache.hugegraph.config.ConfigOption; import org.apache.hugegraph.config.OptionHolder; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.HubbleUtil; -import org.springframework.util.CollectionUtils; public class HubbleOptions extends OptionHolder { @@ -56,7 +57,7 @@ public static synchronized HubbleOptions instance() { public static final ConfigOption SERVER_HOST = new ConfigOption<>( - "hubble.host", + "server.host", "The host of hugegraph-hubble server.", disallowEmpty(), "localhost" @@ -64,7 +65,7 @@ public static synchronized HubbleOptions instance() { public static final ConfigOption SERVER_PORT = new ConfigOption<>( - "hubble.port", + "server.port", "The port of hugegraph-hubble server.", rangeInt(1, 65535), 8088 @@ -102,7 +103,10 @@ public static synchronized HubbleOptions instance() { if (CollectionUtils.isEmpty(input)) { return false; } - return !input.contains(-1) || input.size() <= 1; + if (input.contains(-1) && input.size() > 1) { + return false; + } + return true; }, -1 ); @@ -168,7 +172,7 @@ public static synchronized HubbleOptions instance() { "upload_file.format_list", "The format white list available for uploading file.", null, - "csv" + "csv", "txt" ); public static final ConfigOption UPLOAD_SINGLE_FILE_SIZE_LIMIT = @@ -197,6 +201,38 @@ public static synchronized HubbleOptions instance() { 12L * 60 * 60 ); + public static final ConfigOption LANGCHAIN_PYTHON_PATH = + new ConfigOption<>( + "langchain.python_path", + "The fixed python executable path for LangChain scripts.", + disallowEmpty(), + "python3" + ); + + public static final ConfigOption LANGCHAIN_SCRIPT_DIR = + new ConfigOption<>( + "langchain.script_dir", + "The fixed directory for LangChain scripts.", + disallowEmpty(), + "langchaincode" + ); + + public static final ConfigListOption LANGCHAIN_SCRIPT_ALLOWLIST = + new ConfigListOption<>( + "langchain.script_allowlist", + "The allowed LangChain script file names.", + input -> !CollectionUtils.isEmpty(input), + "excute_langchain.py" + ); + + public static final ConfigOption LANGCHAIN_EXECUTE_TIMEOUT = + new ConfigOption<>( + "langchain.execute_timeout", + "The timeout in seconds for LangChain script execution.", + positiveInt(), + 30 + ); + public static final ConfigOption SERVER_PROTOCOL = new ConfigOption<>( "server.protocol", @@ -223,4 +259,151 @@ public static synchronized HubbleOptions instance() { null, "hugegraph" ); + + public static final ConfigOption PD_ENABLED = + new ConfigOption<>( + "pd.enabled", + "Whether to enable PD mode. Set to false for standalone " + + "RocksDB mode where PD is not available.", + null, + true + ); + + public static final ConfigOption SERVER_URL = + new ConfigOption<>( + "server.direct_url", + "The direct URL of HugeGraph Server, used only when " + + "pd.enabled=false. Example: http://127.0.0.1:8080", + null, + "http://127.0.0.1:8080" + ); + + public static final ConfigOption PD_CLUSTER = + new ConfigOption<>( + "cluster", + "The cluster which hubble connect to", + null, + "hg" + ); + + public static final ConfigOption PD_PEERS = + new ConfigOption<>( + "pd.peers", + "The pd addresses", + null, + "127.0.0.1:8686" + ); + + public static final ConfigOption PD_SERVER = + new ConfigOption<>( + "pd.server", + "The pd-server addresses", + null, + "127.0.0.1:8620" + ); + + public static final ConfigOption DASHBOARD_ADDRESS = + new ConfigOption<>( + "dashboard.address", + "The dashboard addresses", + null, + "127.0.0.1:8092" + ); + + public static final ConfigOption ROUTE_TYPE = + new ConfigOption<>( + "route.type", + "use service url", + allowValues("BOTH", "NODE_PORT", "DDS"), + "NODE_PORT" + ); + + public static final ConfigOption PROMETHEUS_URL = + new ConfigOption<>( + "prometheus.url", + "prometheus url, for saas metrics", + null, + "http://127.0.0.1:8090" + ); + + public static final ConfigOption IDC = + new ConfigOption<>( + "idc", + "hubble deployment location (eg:bddwd or gzbh)", + disallowEmpty(), + "bddwd" + ); + + public static final ConfigOption MONITOR_URL = + new ConfigOption<>( + "monitor.url", + "monitor URL", + null, + "" + ); + + public static final ConfigOption ES_URL = + new ConfigOption<>( + "es.urls", + "The addresses of Elasticsearch Cluster", + null, + "" + ); + + public static final ConfigOption ES_USER = + new ConfigOption<>( + "es.user", + "The user of Elasticsearch Cluster", + null, + "" + ); + + public static final ConfigOption ES_PASSWORD = + new ConfigOption<>( + "es.password", + "The password of Elasticsearch Cluster", + null, + "" + ); + + // ES查询: max_result_window + public static final ConfigOption MAX_RESULT_WINDOW = + new ConfigOption<>( + "es.max_result_window", + "es config info: max_result_window", + positiveInt(), + 10000 + ); + + public static final ConfigOption LOG_AUDIT_PATTERN = + new ConfigOption<>( + "log.audit.pattern", + "the index name of audit log", + null, + "hugegraphaudit" + ); + + public static final ConfigOption LOG_EXPORT_COUNT = + new ConfigOption<>( + "log.export.count", + "max export item count of audit/log", + positiveInt(), + 10000 + ); + + public static final ConfigOption PROXY_SERVLET_URL = + new ConfigOption<>( + "proxy.servlet_url", + "the servlet url to access", + null, + "" + ); + + public static final ConfigOption PROXY_TARGET_URL = + new ConfigOption<>( + "proxy.target_url", + "the target url to access", + null, + "" + ); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/GraphConnectionService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/GraphConnectionService.java deleted file mode 100644 index 0889b666c..000000000 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/GraphConnectionService.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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 org.apache.hugegraph.service; - -import java.util.List; - -import org.apache.hugegraph.entity.GraphConnection; -import org.apache.hugegraph.exception.InternalException; -import org.apache.hugegraph.mapper.GraphConnectionMapper; -import org.apache.hugegraph.util.SQLUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; - -@Service -public class GraphConnectionService { - - @Autowired - private GraphConnectionMapper mapper; - - public List listAll() { - return this.mapper.selectList(null); - } - - public IPage list(String content, long current, - long pageSize) { - IPage page = new Page<>(current, pageSize); - if (!StringUtils.isEmpty(content)) { - String value = SQLUtil.escapeLike(content); - return this.mapper.selectByContentInPage(page, value); - } else { - QueryWrapper query = Wrappers.query(); - query.orderByDesc("create_time"); - return this.mapper.selectPage(page, query); - } - } - - public GraphConnection get(int id) { - return this.mapper.selectById(id); - } - - public int count() { - return this.mapper.selectCount(null); - } - - @Transactional(isolation = Isolation.READ_COMMITTED) - public void save(GraphConnection connection) { - if (this.mapper.insert(connection) != 1) { - throw new InternalException("entity.insert.failed", connection); - } - } - - @Transactional(isolation = Isolation.READ_COMMITTED) - public void update(GraphConnection connection) { - if (this.mapper.updateById(connection) != 1) { - throw new InternalException("entity.update.failed", connection); - } - } - - @Transactional(isolation = Isolation.READ_COMMITTED) - public void remove(int id) { - if (this.mapper.deleteById(id) != 1) { - throw new InternalException("entity.delete.failed", id); - } - } -} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java index 7b210efc3..04904d73d 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java @@ -18,69 +18,236 @@ package org.apache.hugegraph.service; -import java.util.concurrent.ConcurrentHashMap; +import static org.apache.hugegraph.driver.factory.PDHugeClientFactory.DEFAULT_GRAPHSPACE; +import static org.apache.hugegraph.driver.factory.PDHugeClientFactory.DEFAULT_SERVICE; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import javax.annotation.PreDestroy; -import org.apache.hugegraph.config.HugeConfig; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.factory.PDHugeClientFactory; import org.apache.hugegraph.entity.GraphConnection; -import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.options.HubbleOptions; -import org.apache.hugegraph.util.HugeClientUtil; +import org.apache.hugegraph.exception.ParameterizedException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; - import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.config.HugeConfig; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.util.HugeClientUtil; +import org.apache.hugegraph.util.UrlUtil; +import com.google.common.collect.ImmutableList; @Log4j2 @Service -public final class HugeClientPoolService - extends ConcurrentHashMap { +public final class HugeClientPoolService { @Autowired private HugeConfig config; @Autowired - private GraphConnectionService connService; - @Autowired private SettingSSLService sslService; + @Autowired + private String cluster; + @Autowired(required = false) + private PDHugeClientFactory pdHugeClientFactory; + + private final Map clients = new ConcurrentHashMap<>(); + + /** + * cache key format: {graphSpace}_{graph} + */ + private static final String CACHE_KEY_FORMAT = "%s_%s"; + private Cache> urlCache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); @PreDestroy public void destroy() { log.info("Destroy HugeClient pool"); - for (HugeClient client : this.values()) { + for (HugeClient client : this.clients.values()) { client.close(); } } - public void put(GraphConnection connection, HugeClient client) { - super.put(connection.getId(), client); + public HugeClient createUnauthClient() { + // Get all graphspace under cluster + return getOrCreate(null, null, null, null); + } + + public HugeClient createTempTokenClient(String token) { + return getOrCreate(null, null, null, token); + } + + public HugeClient createTempBasicClient(String username, String password) { + return getOrCreate(null, null, null, null, username, password); + } + + public HugeClient createBasicClient(String graphSpace, String graph, + String username, String password) { + return getOrCreate(null, graphSpace, graph, null, username, password); + } + + public HugeClient createAuthClient(String graphSpace, + String graph, String token) { + return getOrCreate(null, graphSpace, graph, token); + } + + public HugeClient getOrCreate(String url, String graphSpace, String graph, + String token) { + // 去掉缓存,固定每个 request 分配一个 client + return getOrCreate(url, graphSpace, graph, token, null, null); } - public synchronized HugeClient getOrCreate(Integer id) { - HugeClient client = super.get(id); - if (client != null) { - return client; + private HugeClient getOrCreate(String url, String graphSpace, String graph, + String token, String username, String password) { + // 去掉缓存,固定每个 request 分配一个 client + return create(url, graphSpace, graph, token, username, password); + } + + public HugeClient create(String url, String graphSpace, String graph, + String token) { + return create(url, graphSpace, graph, token, null, null); + } + + private HugeClient create(String url, String graphSpace, String graph, + String token, String username, String password) { + if (StringUtils.isEmpty(url)) { + List urls = this.allAvailableURLs(graphSpace, graph); + + if (CollectionUtils.isEmpty(urls)) { + throw new ParameterizedException("service.no-available"); + } + + for (String tmpurl : urls) { + if (StringUtils.isEmpty(tmpurl)) { + continue; + } + HugeClient tmpclient = this.create(tmpurl, graphSpace, graph, token, + username, password); + + if (checkHealth(tmpclient)) { + return tmpclient; + } else { + tmpclient.close(); + } + } } - GraphConnection connection = this.connService.get(id); - if (connection == null) { - throw new ExternalException("graph-connection.get.failed", id); + + GraphConnection connection = new GraphConnection(); + try { + UrlUtil.Host host = UrlUtil.parseHost(url); + connection.setProtocol(host.getScheme()); + connection.setHost(host.getHost()); + connection.setPort(host.getPort()); + } catch (IllegalArgumentException e) { + throw new ParameterizedException("service.url.parse.error", e, url); } + + connection.setToken(token); + connection.setUsername(username); + connection.setPassword(password); + connection.setGraphSpace(graphSpace); + connection.setGraph(graph); if (connection.getTimeout() == null) { int timeout = this.config.get(HubbleOptions.CLIENT_REQUEST_TIMEOUT); connection.setTimeout(timeout); } this.sslService.configSSL(this.config, connection); - client = HugeClientUtil.tryConnect(connection); - this.put(id, client); + HugeClient client = HugeClientUtil.tryConnect(connection); + return client; } - public void remove(GraphConnection connection) { - HugeClient client = super.remove(connection.getId()); - if (client == null) { - return; + /** + * 获取所有可用的URL列表 + * + * @param graphSpace 图形空间名称 + * @param service 服务名称 + * @return URL列表 + */ + private List allAvailableURLs(String graphSpace, String service) { + // Non-PD mode: directly return the configured server URL + boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED); + if (!pdEnabled) { + String directUrl = config.get(HubbleOptions.SERVER_URL); + log.info("PD mode disabled, using direct server URL: {}", directUrl); + return ImmutableList.of(directUrl); + } + + List realtimeurls = new ArrayList<>(); + if (StringUtils.isNotEmpty(graphSpace)) { + if (StringUtils.isNotEmpty(service)) { + // Get realtineurls From service + List urls = pdHugeClientFactory.getURLs(cluster, graphSpace, + service); + if (!CollectionUtils.isEmpty(urls)) { + // 打乱顺序 + Collections.shuffle(urls); + realtimeurls.addAll(urls); + } + } + + List urls = pdHugeClientFactory.getURLs(cluster, graphSpace, null); + if (!CollectionUtils.isEmpty(urls)) { + // 打乱顺序 + Collections.shuffle(urls); + realtimeurls.addAll(urls); + } + } + + List urls = pdHugeClientFactory.getURLs(cluster, DEFAULT_GRAPHSPACE, + DEFAULT_SERVICE); + String defaultCacheKey = String.format(CACHE_KEY_FORMAT, DEFAULT_GRAPHSPACE, + DEFAULT_SERVICE); + if (!CollectionUtils.isEmpty(urls)) { + Collections.shuffle(urls); + // 设置默认URL缓存 + urlCache.put(defaultCacheKey, urls); + realtimeurls.addAll(urls); + } + + String cacheKey = String.format(CACHE_KEY_FORMAT, graphSpace, service); + if (!CollectionUtils.isEmpty(realtimeurls)) { + urlCache.put(cacheKey, realtimeurls); + return realtimeurls; + } else { + + List cacheKeys = new ArrayList<>(); + cacheKeys.add(String.format(CACHE_KEY_FORMAT, graphSpace, service)); + cacheKeys.add(String.format(CACHE_KEY_FORMAT, graphSpace, null)); + cacheKeys.add(defaultCacheKey); + for (String ck : cacheKeys) { + List r = urlCache.getIfPresent(ck); + if (!CollectionUtils.isEmpty(r)) { + return r; + } + } + + log.warn("Get empty list of availabel url from cache: {}/{}", + graphSpace, service); + + return ImmutableList.of(); + } + } + + private boolean checkHealth(HugeClient client) { + try { + client.versionManager().getApiVersion(); + } catch (Exception e) { + log.debug("Check client health throw exception", e); + return false; } - client.close(); + + return true; } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/SettingSSLService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/SettingSSLService.java index 3ab2b549c..d3bb6ce46 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/SettingSSLService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/SettingSSLService.java @@ -18,25 +18,24 @@ package org.apache.hugegraph.service; +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.entity.GraphConnection; import org.apache.hugegraph.options.HubbleOptions; import org.springframework.stereotype.Service; -import lombok.extern.log4j.Log4j2; - @Log4j2 @Service public class SettingSSLService { public void configSSL(HugeConfig config, GraphConnection connection) { String protocol = config.get(HubbleOptions.SERVER_PROTOCOL); - if ("https".equals(protocol)) { + if (protocol != null && protocol.equals("https")) { connection.setProtocol(protocol); String trustStoreFile = config.get( - HubbleOptions.CLIENT_TRUSTSTORE_FILE); + HubbleOptions.CLIENT_TRUSTSTORE_FILE); String trustStorePass = config.get( - HubbleOptions.CLIENT_TRUSTSTORE_PASSWORD); + HubbleOptions.CLIENT_TRUSTSTORE_PASSWORD); connection.setTrustStoreFile(trustStoreFile); connection.setTrustStorePassword(trustStorePass); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/AsyncTaskService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/AsyncTaskService.java index a509b3e2a..2255ca727 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/AsyncTaskService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/AsyncTaskService.java @@ -18,49 +18,67 @@ package org.apache.hugegraph.service.algorithm; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.driver.HugeClient; -import org.apache.hugegraph.service.HugeClientPoolService; import org.apache.hugegraph.structure.Task; import org.apache.hugegraph.util.PageUtil; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.baomidou.mybatisplus.core.metadata.IPage; - -import lombok.extern.log4j.Log4j2; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; @Log4j2 @Service public class AsyncTaskService { - @Autowired - private HugeClientPoolService poolService; + private static final String VERMEER_TASK_TYPE = "vermeer-task"; + private static final String VERMEER_TASK_LOAD = "vermeer-task:load"; + private static final String VERMEER_TASK_COMPUTE = "vermeer-task:compute"; - private HugeClient getClient(int connId) { - return this.poolService.getOrCreate(connId); + public Task get(HugeClient client, int id) { + Task task = client.task().get(id); + if (isVermeerType(task.type())) { + task.type(switchVermeerType(task.name())); + } + return task; } - public Task get(int connId, int id) { - HugeClient client = this.getClient(connId); - return client.task().get(id); + private boolean isVermeerType(String type) { + return VERMEER_TASK_TYPE.equals(type); + } + + private String switchVermeerType(String name) { + if (VERMEER_TASK_LOAD.equals(name)) { + return name; + } + return VERMEER_TASK_COMPUTE; } - public List list(int connId, List taskIds) { - HugeClient client = this.getClient(connId); - return client.task().list(taskIds); + public List list(HugeClient client, List taskIds) { + List tasks = client.task().list(taskIds); + for (Task task: tasks) { + if (isVermeerType(task.type())) { + task.type(switchVermeerType(task.name())); + } + } + return tasks; } - public IPage list(int connId, int pageNo, int pageSize, String content, + public IPage list(HugeClient client, int pageNo, int pageSize, + String content, String type, String status) { - HugeClient client = this.getClient(connId); if (status.isEmpty()) { status = null; } List tasks = client.task().list(status); + for (Task task: tasks) { + if (isVermeerType(task.type())) { + task.type(switchVermeerType(task.name())); + } + } + List result = new ArrayList<>(); for (Task task : tasks) { if (!type.isEmpty() && !type.equals(task.type())) { @@ -78,13 +96,11 @@ public IPage list(int connId, int pageNo, int pageSize, String content, return PageUtil.page(result, pageNo, pageSize); } - public void remove(int connId, int id) { - HugeClient client = this.getClient(connId); + public void remove(HugeClient client, int id) { client.task().delete(id); } - public Task cancel(int connId, int id) { - HugeClient client = this.getClient(connId); + public Task cancel(HugeClient client, int id) { return client.task().cancel(id); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OlapAlgoService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OlapAlgoService.java new file mode 100644 index 000000000..be3b8abcc --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OlapAlgoService.java @@ -0,0 +1,54 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.algorithm; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.algorithm.OlapEntity; +import org.apache.hugegraph.entity.query.OlapView; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Log4j2 +@Service +public class OlapAlgoService { + @Autowired + private HugeConfig config; + @Autowired + private ExecuteHistoryService historyService; + + public OlapView olapView(HugeClient client, String graphspace, OlapEntity body) { + Map params = body.getParams(); + if (!"DEFAULT".equals(graphspace)) { + params.put("k8s.master_cpu", "1"); + params.put("k8s.worker_cpu", "1"); + params.put("k8s.master_request_memory", "5Gi"); + params.put("k8s.worker_request_memory", "5Gi"); + params.put("k8s.master_memory", "5Gi"); + params.put("k8s.worker_memory", "5Gi"); + params.putAll(body.getParams()); + } + long taskid = client.computer().create(body.getAlgorithm(), body.getWorker(), params); + return OlapView.builder().taskId(taskid).build(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OltpAlgoService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OltpAlgoService.java index a98753525..b882d77ef 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OltpAlgoService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OltpAlgoService.java @@ -18,109 +18,1341 @@ package org.apache.hugegraph.service.algorithm; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +// TODO import difference +import org.apache.hugegraph.api.traverser.NeighborRankAPI; +import org.apache.hugegraph.api.traverser.PersonalRankAPI; +//import org.apache.hugegraph.client.api.traverser.PersonalRankAPI; +//import org.apache.hugegraph.client.api.traverser.NeighborRankAPI; +import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.driver.TraverserManager; -import org.apache.hugegraph.entity.algorithm.ShortestPath; +import org.apache.hugegraph.entity.algorithm.AdamicadarEntity; +import org.apache.hugegraph.entity.algorithm.AllShortestPathsEntity; +import org.apache.hugegraph.entity.algorithm.CrossPointsEntity; +import org.apache.hugegraph.entity.algorithm.JaccardSimilarityEntity; +import org.apache.hugegraph.entity.algorithm.KneighborEntity; +import org.apache.hugegraph.entity.algorithm.KoutEntity; +import org.apache.hugegraph.entity.algorithm.PathsEntity; +import org.apache.hugegraph.entity.algorithm.RaysEntity; +import org.apache.hugegraph.entity.algorithm.ResourceallocationEntity; +import org.apache.hugegraph.entity.algorithm.SingleSourceShortestPathEntity; +import org.apache.hugegraph.entity.algorithm.WeightedShortestPathEntity; +import org.apache.hugegraph.entity.query.EgonetView; +import org.apache.hugegraph.entity.query.FusiformsimilarityView; +import org.apache.hugegraph.entity.query.JaccardsimilarityView; +import org.apache.hugegraph.entity.query.RanksView; +import org.apache.hugegraph.structure.traverser.CrosspointsRequest; +import org.apache.hugegraph.structure.traverser.CustomizedCrosspoints; +import org.apache.hugegraph.structure.traverser.CustomizedPathsRequest; +import org.apache.hugegraph.structure.traverser.Egonet; +import org.apache.hugegraph.structure.traverser.EgonetRequest; +import org.apache.hugegraph.structure.traverser.FusiformSimilarity; +import org.apache.hugegraph.structure.traverser.FusiformSimilarityRequest; +import org.apache.hugegraph.structure.traverser.JaccardSimilarity; +import org.apache.hugegraph.structure.traverser.Kneighbor; +import org.apache.hugegraph.structure.traverser.KneighborRequest; +import org.apache.hugegraph.structure.traverser.Kout; +import org.apache.hugegraph.entity.algorithm.RingsEntity; +import org.apache.hugegraph.entity.algorithm.SameNeighborsEntity; +import org.apache.hugegraph.entity.algorithm.ShortestPathEntity; import org.apache.hugegraph.entity.enums.AsyncTaskStatus; import org.apache.hugegraph.entity.enums.ExecuteStatus; import org.apache.hugegraph.entity.enums.ExecuteType; import org.apache.hugegraph.entity.query.ExecuteHistory; import org.apache.hugegraph.entity.query.GraphView; import org.apache.hugegraph.entity.query.GremlinResult; -import org.apache.hugegraph.entity.query.JsonView; -import org.apache.hugegraph.entity.query.TableView; -import org.apache.hugegraph.service.HugeClientPoolService; +import org.apache.hugegraph.options.HubbleOptions; import org.apache.hugegraph.service.query.ExecuteHistoryService; import org.apache.hugegraph.structure.graph.Edge; import org.apache.hugegraph.structure.graph.Path; import org.apache.hugegraph.structure.graph.Vertex; +import org.apache.hugegraph.structure.gremlin.Result; +import org.apache.hugegraph.structure.gremlin.ResultSet; +import org.apache.hugegraph.structure.traverser.KoutRequest; +import org.apache.hugegraph.structure.traverser.MultiNodeShortestPathRequest; +import org.apache.hugegraph.structure.traverser.PathOfVertices; +import org.apache.hugegraph.structure.traverser.PathWithMeasure; +import org.apache.hugegraph.structure.traverser.PathsRequest; +import org.apache.hugegraph.structure.traverser.PathsWithVertices; +import org.apache.hugegraph.structure.traverser.Prediction; +import org.apache.hugegraph.structure.traverser.RanksWithMeasure; +import org.apache.hugegraph.structure.traverser.SameNeighbors; +import org.apache.hugegraph.structure.traverser.SameNeighborsBatch; +import org.apache.hugegraph.structure.traverser.SameNeighborsBatchRequest; +import org.apache.hugegraph.structure.traverser.SingleSourceJaccardSimilarityRequest; +import org.apache.hugegraph.structure.traverser.Steps; +import org.apache.hugegraph.structure.traverser.TemplatePathsRequest; +import org.apache.hugegraph.structure.traverser.WeightedPath; +import org.apache.hugegraph.structure.traverser.WeightedPaths; +import org.apache.hugegraph.util.GremlinUtil; import org.apache.hugegraph.util.HubbleUtil; +import com.google.common.collect.Iterables; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.StopWatch; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.google.common.collect.ImmutableMap; - -import lombok.extern.log4j.Log4j2; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @Log4j2 @Service public class OltpAlgoService { - @Autowired - private HugeClientPoolService poolService; + private HugeConfig config; @Autowired private ExecuteHistoryService historyService; - private HugeClient getClient(int connId) { - return this.poolService.getOrCreate(connId); + public GremlinResult shortestPath(HugeClient client, ShortestPathEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + PathOfVertices result = traverser.shortestPath(body.getSource(), body.getTarget(), + body.getDirection(), body.getLabel(), + body.getMaxDepth(), body.getMaxDegree(), + body.getSkipDegree(), body.getCapacity()); + GraphView graphView = this.buildPathGraphView(client, result.getPath()); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(graphView) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult allShortestPaths(HugeClient client, AllShortestPathsEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + PathWithMeasure result = traverser.allShortestPaths(body.getSource(), body.getTarget(), + body.getDirection(), body.getLabel(), + body.getMaxDepth(), body.getMaxDegree(), + body.getSkipDegree(), body.getCapacity()); + GraphView graphView = this.buildPathGraphView(client, result.getPaths()); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(graphView) + .pathnum(result.getPaths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } } - public GremlinResult shortestPath(int connId, ShortestPath body) { - HugeClient client = this.getClient(connId); - TraverserManager traverser = client.traverser(); - Path result = traverser.shortestPath(body.getSource(), body.getTarget(), - body.getDirection(), body.getLabel(), - body.getMaxDepth(), body.getMaxDegree(), - body.getSkipDegree(), body.getCapacity()).getPath(); - JsonView jsonView = new JsonView(); - jsonView.setData(result.objects()); + public GremlinResult weightedShortestPath(HugeClient client, WeightedShortestPathEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges = new HashMap<>(); + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + WeightedPath result = traverser.weightedShortestPath(body.getSource(), body.getTarget(), + body.getDirection(), body.getLabel(), + body.getWeight(), body.getMaxDegree(), + body.getSkipDegree(), body.getCapacity(), true, true); + + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + for (Iterator iter = result.edges().iterator(); iter.hasNext();) { + Edge edge = iter.next(); + edges.put(edge.id(), edge); + } + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult singleSourceShortestPath(HugeClient client, + SingleSourceShortestPathEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + // TODO edges info set as false, check correctness + WeightedPaths result = traverser.singleSourceShortestPath(body.getSource(), + body.getDirection(), body.getLabel(), + body.getWeight(), body.getMaxDegree(), + body.getSkipDegree(), body.getCapacity(), body.getLimit(), true,false); + + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult multiNodeShortestPath(HugeClient client, + MultiNodeShortestPathRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + body.withVertex = true; + PathsWithVertices result = traverser.multiNodeShortestPath(body); + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult rings(HugeClient client, RingsEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); Date createTime = HubbleUtil.nowDate(); - TableView tableView = this.buildPathTableView(result); - GraphView graphView = this.buildPathGraphView(result); // Insert execute history - ExecuteStatus status = ExecuteStatus.SUCCESS; + ExecuteStatus status = ExecuteStatus.RUNNING; ExecuteHistory history; - history = new ExecuteHistory(null, connId, 0L, ExecuteType.ALGORITHM, - body.toString(), status, - AsyncTaskStatus.UNKNOWN, -1L, createTime); + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); this.historyService.save(history); - return GremlinResult.builder() - .type(GremlinResult.Type.PATH) - .jsonView(jsonView) - .tableView(tableView) - .graphView(graphView) - .build(); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + PathWithMeasure result = traverser.rings(body.getSource(), + body.getDirection(), body.getLabel(), + body.getMaxDepth(), body.isSourceInRing(), + body.getMaxDegree(), body.getCapacity(), + body.getLimit()); + GraphView graphView = this.buildPathGraphView(client, result.getRings()); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(graphView) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } } - private TableView buildPathTableView(Path result) { - List elements = result.objects(); - List paths = new ArrayList<>(elements.size()); - List ids = new ArrayList<>(); - elements.forEach(element -> { - if (element instanceof Vertex) { - ids.add(((Vertex) element).id()); - } else if (element instanceof Edge) { - ids.add(((Edge) element).id()); - } else { - ids.add(element); + public GremlinResult advancedpaths(HugeClient client, PathsRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + body.withVertex = true; + PathsWithVertices result = traverser.paths(body); + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); } - }); - paths.add(ImmutableMap.of("path", ids)); - return new TableView(TableView.PATH_HEADER, paths); + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult paths(HugeClient client, PathsEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + PathWithMeasure result = traverser.paths(body.getSource(), + body.getTarget(), body.getDirection(), body.getLabel(), + body.getMaxDepth(), body.getMaxDegree(), body.getCapacity(), + body.getLimit()); + GraphView graphView = this.buildPathGraphView(client, result.getPaths()); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(graphView) + .pathnum(result.getPaths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult sameNeighbors(HugeClient client, SameNeighborsEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + SameNeighbors result = traverser.sameNeighbors(body.getVertex(), + body.getOther(), body.getDirection(), body.getLabel(), + body.getMaxDegree(), body.getLimit()); + List resultVertices = result.getSameNeighbors(); + resultVertices.add(body.getVertex()); + resultVertices.add(body.getOther()); + List escapedIds = resultVertices.stream() + .map(GremlinUtil::escapeId).collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + String gremlin = String.format("g.V(%s).limit(1000)", ids); + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + for (Iterator iter = resultSet.iterator(); iter.hasNext(); ) { + Vertex vertex = iter.next().getVertex(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult kout(HugeClient client, KoutEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + KoutRequest.Builder builder = KoutRequest.builder(); + Steps.StepEntity stepEntity = new Steps.StepEntity(body.getLabel()); + builder.source(body.getSource()).maxDepth(body.getMaxDepth()). + nearest(body.isNearest()).capacity(body.getCapacity()).limit(body.getLimit()). + withPath(false).withVertex(true).withEdge(true). + steps().direction(body.getDirection()).degree(body.getMaxDegree()) + .skipDegree(body.getMaxDegree()).edgeSteps(stepEntity); + Kout result = traverser.kout(builder.build()); + Map edges = new HashMap<>(); + for (Iterator iter = result.edges().iterator(); iter.hasNext();) { + Edge edge = iter.next(); + edges.put(edge.id(), edge); + } + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView( + this.verticesOfEdge(edges, client).values(), + result.edges())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } } - private GraphView buildPathGraphView(Path result) { + public GremlinResult koutPost(HugeClient client, KoutRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + body.withVertex = true; + body.withEdge = true; + body.countOnly = false; + Kout result = traverser.kout(body); + Map edges = new HashMap<>(); + for (Iterator iter = result.edges().iterator(); iter.hasNext();) { + Edge edge = iter.next(); + edges.put(edge.id(), edge); + } + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView( + this.verticesOfEdge(edges, client).values(), + result.edges())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult kneighbor(HugeClient client, KneighborEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + KneighborRequest.Builder builder = KneighborRequest.builder(); + Steps.StepEntity stepEntity = new Steps.StepEntity(body.getLabel()); + builder.source(body.getSource()).maxDepth(body.getMaxDepth()). + limit(body.getLimit()). + withPath(true).withVertex(true).withEdge(true). + steps().direction(body.getDirection()).degree(body.getMaxDegree()) + .edgeSteps(stepEntity); + Kneighbor result = traverser.kneighbor(builder.build()); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(result.vertices(), result.edges())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult kneighborPost(HugeClient client, KneighborRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + body.withVertex = true; + body.withEdge = true; + body.withPath = true; + body.countOnly = false; + Kneighbor result = traverser.kneighbor(body); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(result.vertices(), result.edges())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public JaccardsimilarityView jaccardSimilarity(HugeClient client, + JaccardSimilarityEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + JaccardSimilarity result = traverser.jaccardSimilarity( + body.getVertex(), body.getOther(), + body.getDirection(), body.getLabel(), body.getMaxDegree()); + status = ExecuteStatus.SUCCESS; + return JaccardsimilarityView.builder() + .jaccardsimilarity(result.getJaccardSimilarity()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public JaccardsimilarityView jaccardSimilarityPost(HugeClient client, + SingleSourceJaccardSimilarityRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + JaccardSimilarity result = traverser.jaccardSimilarity(body); + status = ExecuteStatus.SUCCESS; + return JaccardsimilarityView.builder() + .jaccardsimilarity(result.getJaccardSimilarity()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public RanksView personalRank(HugeClient client, PersonalRankAPI.Request body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + RanksWithMeasure result = traverser.personalRank(body); + status = ExecuteStatus.SUCCESS; + return RanksView.builder() + .ranks(result.personalRanks()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public RanksView neighborRank(HugeClient client, NeighborRankAPI.Request body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + RanksWithMeasure result = traverser.neighborRank(body); + status = ExecuteStatus.SUCCESS; + return RanksView.builder() + .ranksList(result.ranks()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult customizedPaths(HugeClient client, CustomizedPathsRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + body.withVertex = true; + PathsWithVertices result = traverser.customizedPaths(body); + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult templatePaths(HugeClient client, TemplatePathsRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + body.withVertex = true; + PathsWithVertices result = traverser.count(body); + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult crosspoints(HugeClient client, CrossPointsEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + PathWithMeasure result = traverser.crosspoint(body.getSource(), + body.getTarget(), body.getDirection(), body.getLabel(), + body.getMaxDepth(), body.getMaxDegree(), body.getCapacity(), + body.getLimit()); + Map vertices = new HashMap<>(); + Map edges = new HashMap<>(); + List elements = new ArrayList(); + for (Path crosspoints : result.getCrosspoints()) { + elements.add(crosspoints.crosspoint()); + } + List escapedIds = elements.stream() + .map(GremlinUtil::escapeId) + .collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + GraphView graphView = new GraphView(vertices.values(), edges.values()); + if (!ids.isEmpty()) { + String gremlin = String.format("g.V(%s).limit(1000)", ids); + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + for (Iterator iter = resultSet.iterator(); iter.hasNext();) { + Vertex vertex = iter.next().getVertex(); + vertices.put(vertex.id(), vertex); + } + graphView = new GraphView(vertices.values(), edges.values()); + } + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(graphView) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult customizedcrosspoints(HugeClient client, CrosspointsRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + body.withVertex = true; + body.withPath = true; + CustomizedCrosspoints result = traverser.customizedCrosspointss(body); + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .pathnum(result.paths().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult rays(HugeClient client, RaysEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + PathWithMeasure result = traverser.rays(body.getSource(), + body.getDirection(), body.getLabel(), + body.getMaxDepth(), body.getMaxDegree(), body.getCapacity(), + body.getLimit()); + GraphView graphView = this.buildPathGraphView(client, result.getRays()); + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(graphView) + .pathnum(result.getRays().size()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public FusiformsimilarityView fusiformsimilarity( + HugeClient client, FusiformSimilarityRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + body.withVertex = true; + FusiformSimilarity result = traverser.fusiformSimilarity(body); + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return FusiformsimilarityView.builder() + .graphView(new GraphView(vertices.values(), edges.values())) + .similarsMap(result.similarsMap()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public Map adamicadar(HugeClient client, AdamicadarEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + Prediction result = traverser.adamicadar(body.getVertex(), body.getOther(), + body.getDirection(), body.getLabel(), + body.getMaxDegree()); + status = ExecuteStatus.SUCCESS; + Map resultMap = new HashMap<>(); + resultMap.put("adamic_adar", result.getAdamicAdar()); + return resultMap; + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public Map resourceallocation(HugeClient client, + ResourceallocationEntity body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + Prediction result = traverser.resourceAllocation(body.getVertex(), body.getOther(), + body.getDirection(), body.getLabel(), + body.getMaxDegree()); + status = ExecuteStatus.SUCCESS; + Map resultMap = new HashMap<>(); + resultMap.put("resource_allocation", result.getResourceAllocation()); + return resultMap; + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + public GremlinResult sameneighborsbatch(HugeClient client, SameNeighborsBatchRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + TraverserManager traverser = client.traverser(); + SameNeighborsBatch result = traverser.sameNeighbors(body); + Map edges = new HashMap<>(); + Map vertices = new HashMap<>(); + List> elementlist = result.sameNeighbors; + for (List elements : elementlist) { + List escapedIds = elements.stream() + .map(GremlinUtil::escapeId) + .collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + if (!ids.isEmpty()) { + String gremlin = String.format("g.V(%s).limit(1000)", ids); + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + for (Iterator iter = resultSet.iterator(); iter.hasNext(); ) { + Vertex vertex = iter.next().getVertex(); + vertices.put(vertex.id(), vertex); + } + edges.putAll(this.edgesOfVertex(vertices, client)); + } + } + status = ExecuteStatus.SUCCESS; + return GremlinResult.builder() + .type(GremlinResult.Type.PATH) + .graphView(new GraphView(vertices.values(), edges.values())) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + private GraphView buildPathGraphView(HugeClient client, List results) { + Map edges = new HashMap<>(); Map vertices = new HashMap<>(); + for (Path result : results) { + this.fillPathGraph(client, result, vertices, edges); + } + + return new GraphView(vertices.values(), edges.values()); + } + + public EgonetView egonet(HugeClient client, EgonetRequest body) { + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.ALGORITHM, + body.toString(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + StopWatch timer = StopWatch.createStarted(); + try { + Map edges; + Map vertices = new HashMap<>(); + TraverserManager traverser = client.traverser(); + body.withVertex = true; + body.countOnly = false; + Egonet result = traverser.egonet(body); + for (Iterator iter = result.vertices().iterator(); iter.hasNext();) { + Vertex vertex = iter.next(); + vertices.put(vertex.id(), vertex); + } + edges = this.edgesOfVertex(vertices, client); + status = ExecuteStatus.SUCCESS; + return EgonetView.builder() + .graphView(new GraphView(vertices.values(), edges.values())) + .ids(result.ids()) + .build(); + } catch (Throwable e) { + status = ExecuteStatus.FAILED; + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + this.historyService.update(history); + } + } + + private GraphView buildPathGraphView(HugeClient client, Path result) { Map edges = new HashMap<>(); + Map vertices = new HashMap<>(result.size()); + this.fillPathGraph(client, result, vertices, edges); + return new GraphView(vertices.values(), edges.values()); + } - List elements = result.objects(); - for (Object element : elements) { + private void fillPathGraph(HugeClient client, Path result, + Map vertices, + Map edges) { + Set vertexIds = new LinkedHashSet<>(); + List pathEdges = new ArrayList<>(); + for (Object element : result.objects()) { if (element instanceof Vertex) { Vertex vertex = (Vertex) element; vertices.put(vertex.id(), vertex); } else if (element instanceof Edge) { Edge edge = (Edge) element; edges.put(edge.id(), edge); + pathEdges.add(edge); } else { - return GraphView.EMPTY; + this.addAbsentVertexId(element, vertices, vertexIds); } } - return new GraphView(vertices.values(), new ArrayList<>()); + for (Edge edge : pathEdges) { + this.addAbsentVertexId(edge.sourceId(), vertices, vertexIds); + this.addAbsentVertexId(edge.targetId(), vertices, vertexIds); + } + if (vertexIds.isEmpty()) { + return; + } + List escapedIds = vertexIds.stream() + .map(GremlinUtil::escapeId) + .collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + if (ids.isEmpty()) { + return; + } + String gremlin = String.format("g.V(%s).limit(1000)", ids); + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + for (Iterator iter = resultSet.iterator(); iter.hasNext();) { + Vertex vertex = iter.next().getVertex(); + vertices.put(vertex.id(), vertex); + } + if (pathEdges.isEmpty()) { + edges.putAll(this.edgesOfVertex(vertices, client)); + } + } + + private void addAbsentVertexId(Object vertexId, Map vertices, + Set vertexIds) { + if (vertexId == null) { + return; + } + if (!vertices.containsKey(vertexId)) { + vertexIds.add(vertexId); + } + } + + private Map edgesOfVertex(Map vertices, + HugeClient client) { + int batchSize = this.config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS); + int edgeLimit = this.config.get(HubbleOptions.GREMLIN_EDGES_TOTAL_LIMIT); + int degreeLimit = this.config.get(HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT); + + Set vertexIds = vertices.keySet(); + Map edges = new HashMap<>(vertexIds.size()); + Iterables.partition(vertexIds, batchSize).forEach(batch -> { + List escapedIds = batch.stream() + .map(GremlinUtil::escapeId) + .collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + // Any better way to find two vertices has linked? + String gremlin; + gremlin = String.format("g.V(%s).bothE().local(limit(%s)).dedup()", + ids, degreeLimit); + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + // The edges count for per vertex + Map degrees = new HashMap<>(resultSet.size()); + for (Iterator iter = resultSet.iterator(); iter.hasNext();) { + Edge edge = iter.next().getEdge(); + Object source = edge.sourceId(); + Object target = edge.targetId(); + // only add the interconnected edges of the found vertices + if (!vertexIds.contains(source) || !vertexIds.contains(target)) { + continue; + } + edges.put(edge.id(), edge); + if (edges.size() >= edgeLimit) { + break; + } + + int deg = degrees.compute(source, (k, v) -> v == null ? 1 : v + 1); + if (deg >= degreeLimit) { + break; + } + deg = degrees.compute(target, (k, v) -> v == null ? 1 : v + 1); + if (deg >= degreeLimit) { + break; + } + } + }); + return edges; + } + + private Map verticesOfEdge(Map edges, + HugeClient client) { + int batchSize = this.config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS); + + Set vertexIds = new HashSet<>(edges.size() * 2); + edges.values().forEach(edge -> { + vertexIds.add(edge.sourceId()); + vertexIds.add(edge.targetId()); + }); + + Map vertices = new HashMap<>(vertexIds.size()); + Iterables.partition(vertexIds, batchSize).forEach(batch -> { + List escapedIds = batch.stream() + .map(GremlinUtil::escapeId) + .collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + String gremlin = String.format("g.V(%s)", ids); + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + for (Iterator iter = resultSet.iterator(); iter.hasNext();) { + Vertex vertex = iter.next().getVertex(); + vertices.put(vertex.id(), vertex); + } + }); + return vertices; } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AccessService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AccessService.java new file mode 100644 index 000000000..542b81642 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AccessService.java @@ -0,0 +1,211 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.AccessEntity; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.structure.auth.Access; +import org.apache.hugegraph.structure.auth.HugePermission; +import org.apache.hugegraph.structure.auth.Group; +import org.apache.hugegraph.structure.auth.Target; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +public class AccessService extends AuthService { + + @Autowired + private TargetService targetService; + + public AccessEntity get(HugeClient client, String accessId) { + return this.get(client, null, accessId); + } + + public AccessEntity get(HugeClient client, String graphSpace, + String accessId) { + Access access = client.auth().getAccess(accessId); + if (access == null) { + throw new ExternalException("auth.access.not-exist.id", accessId); + } + Group group = RoleService.getGroup(client.auth(), + access.group().toString()); + Target target = this.targetService.get(client, + access.target().toString()); + return convert(graphSpace, access, group, target); + } + + private List list0(HugeClient client, String roleId, + String targetId) { + List result = new ArrayList<>(); + client.auth().listAccessesByGroup(roleId, -1).forEach(access -> { + if (targetId == null || + access.target().toString().equals(targetId)) { + result.add(access); + } + }); + return result; + } + + public List list(HugeClient client, String roleId, + String targetId) { + return this.list(client, null, roleId, targetId); + } + + public List list(HugeClient client, String graphSpace, + String roleId, String targetId) { + List result = new ArrayList<>(); + List accesses = this.list0(client, roleId, targetId); + Multimap, Access> grouped = + ArrayListMultimap.create(); + + accesses.forEach(access -> { + grouped.put(ImmutableList.of(access.group().toString(), + access.target().toString()), access); + }); + + for (ImmutableList key : grouped.keySet()) { + try { + Group group = RoleService.getGroup(client.auth(), key.get(0)); + Target target = this.targetService.get(client, key.get(1)); + result.add(convert(graphSpace, grouped.get(key), group, + target)); + } catch (Exception e) { + log.warn("list access error", e); + } + } + return result; + } + + public AccessEntity addOrUpdate(HugeClient client, + AccessEntity accessEntity) { + return this.addOrUpdate(client, null, accessEntity); + } + + public AccessEntity addOrUpdate(HugeClient client, String graphSpace, + AccessEntity accessEntity) { + List accesses = this.list0(client, accessEntity.getRoleId(), + accessEntity.getTargetId()); + Set currentPermissions = + accesses.stream().map(Access::permission) + .collect(Collectors.toSet()); + + accesses.forEach(access -> { + if (!accessEntity.getPermissions().contains(access.permission())) { + client.auth().deleteAccess(access.id()); + } + }); + + accessEntity.getPermissions().forEach(permission -> { + if (!currentPermissions.contains(permission)) { + Access access = new Access(); + access.group(accessEntity.getRoleId()); + access.target(accessEntity.getTargetId()); + access.permission(permission); + client.auth().createAccess(access); + } + }); + + List results = this.list(client, graphSpace, + accessEntity.getRoleId(), + accessEntity.getTargetId()); + if (results.isEmpty()) { + return null; + } + return results.get(0); + } + + public void delete(HugeClient client, String roleId, String targetId) { + this.list0(client, roleId, targetId).forEach(access -> { + client.auth().deleteAccess(access.id()); + }); + } + + protected AccessEntity convert(Access access, Group group, Target target) { + return this.convert(null, access, group, target); + } + + protected AccessEntity convert(String graphSpace, Access access, + Group group, Target target) { + AccessEntity entity = new AccessEntity(target.id().toString(), + target.name(), + group.id().toString(), + group.name(), + firstNonNull(graphSpace, + access.graphSpace(), + target.graphSpace()), + target.graph(), + new HashSet<>(), + target.description(), + target.resourcesList()); + entity.addPermission(access.permission()); + return entity; + } + + protected AccessEntity convert(Collection accesses, Group group, + Target target) { + return this.convert(null, accesses, group, target); + } + + protected AccessEntity convert(String graphSpace, + Collection accesses, Group group, + Target target) { + AccessEntity entity = new AccessEntity(target.id().toString(), + target.name(), + group.id().toString(), + group.name(), + firstNonNull(graphSpace, + target.graphSpace()), + target.graph(), + new HashSet<>(), + target.description(), + target.resourcesList()); + accesses.forEach(access -> { + entity.addPermission(access.permission()); + }); + return entity; + } + + private static String firstNonNull(String value, String fallback) { + return value != null ? value : fallback; + } + + private static String firstNonNull(String value, String fallback, + String defaultValue) { + if (value != null) { + return value; + } + if (fallback != null) { + return fallback; + } + return defaultValue; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthService.java new file mode 100644 index 000000000..cc8dca1e6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthService.java @@ -0,0 +1,22 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +public class AuthService { +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/BelongService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/BelongService.java new file mode 100644 index 000000000..eca7734b6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/BelongService.java @@ -0,0 +1,211 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.driver.AuthManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.BelongEntity; +import org.apache.hugegraph.entity.auth.UserEntity; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.structure.auth.Belong; +import org.apache.hugegraph.structure.auth.Group; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +public class BelongService extends AuthService { + + @Autowired + private UserService userService; + + public void add(HugeClient client, String roleId, String userId) { + Belong belong = new Belong(); + belong.user(userId); + belong.group(roleId); + this.add(client, belong); + } + + public void add(HugeClient client, Belong belong) { + client.auth().createBelong(belong); + } + + public void addMany(HugeClient client, String roleId, String[] userIds) { + for (String userId : userIds) { + this.add(client, roleId, userId); + } + } + + public void delete(HugeClient client, String belongId) { + client.auth().deleteBelong(belongId); + } + + public void delete(HugeClient client, String roleId, String userId) { + this.list(client, roleId, userId).forEach(belong -> { + client.auth().deleteBelong(belong.getId()); + }); + } + + protected List listByUser(HugeClient client, String userId) { + AuthManager auth = client.auth(); + List result = new ArrayList<>(); + auth.listBelongsByUser(userId, -1).forEach(belong -> { + BelongEntity entity = this.convert(client, belong); + if (entity != null) { + result.add(entity); + } + }); + return result; + } + + public List listByRole(HugeClient client, String roleId) { + RoleService.getGroup(client.auth(), roleId); + List result = new ArrayList<>(); + client.auth().listBelongsByGroup(roleId, -1).forEach(belong -> { + BelongEntity entity = this.convert(client, belong); + if (entity != null) { + result.add(entity); + } + }); + return result; + } + + public List listAll(HugeClient client) { + List result = new ArrayList<>(); + client.auth().listBelongs().forEach(belong -> { + BelongEntity entity = this.convert(client, belong); + if (entity != null) { + result.add(entity); + } + }); + return result; + } + + public List list(HugeClient client, String roleId, + String userId) { + List result = new ArrayList<>(); + if (StringUtils.isEmpty(userId) && StringUtils.isEmpty(roleId)) { + return this.listAll(client); + } else if (StringUtils.isEmpty(userId)) { + return this.listByRole(client, roleId); + } else if (StringUtils.isEmpty(roleId)) { + return this.listByUser(client, userId); + } + + client.auth().listBelongsByGroup(roleId, -1).forEach(belong -> { + BelongEntity entity = this.convert(client, belong); + if (entity != null && entity.getUserId().equals(userId)) { + result.add(entity); + } + }); + return result; + } + + public IPage listPage(HugeClient client, String roleId, + String userId, int pageNo, + int pageSize) { + return PageUtil.page(this.list(client, roleId, userId), pageNo, + pageSize); + } + + public BelongEntity get(HugeClient client, String belongId) { + Belong belong = client.auth().getBelong(belongId); + if (belong == null) { + throw new InternalException("auth.belong.get.%s Not Exits", + belongId); + } + return this.convert(client, belong); + } + + protected BelongEntity convert(HugeClient client, Belong belong) { + try { + Group group = RoleService.getGroup(client.auth(), + belong.group().toString()); + UserEntity user = this.userService.getUser(client, + belong.user().toString()); + return new BelongEntity(belong.id().toString(), + user.getId(), user.getName(), + group.id().toString(), group.name(), + user.getDescription(), user.getCreate()); + } catch (Exception e) { + log.warn("convert belong error", e); + return null; + } + } + + public void deleteMany(HugeClient client, String[] ids) { + Arrays.stream(ids).forEach(id -> { + client.auth().deleteBelong(id); + }); + } + + public boolean exists(HugeClient client, String roleId, String userId) { + return !this.list(client, roleId, userId).isEmpty(); + } + + public static class BelongsReq { + + @JsonProperty("user_ids") + private Set userIds = new HashSet<>(); + + @JsonProperty("role_id") + private String roleId; + + @JsonProperty("belong_description") + private String description; + + public Set getUserIds() { + return this.userIds; + } + + public BelongsReq setUserIds(Set userIds) { + this.userIds = userIds; + return this; + } + + public String getRoleId() { + return this.roleId; + } + + public BelongsReq setRoleId(String roleId) { + this.roleId = roleId; + return this; + } + + public String getDescription() { + return this.description; + } + + public BelongsReq setDescription(String description) { + this.description = description; + return this; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java new file mode 100644 index 000000000..61a78d301 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java @@ -0,0 +1,159 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.BelongEntity; +import org.apache.hugegraph.entity.auth.RoleEntity; +import org.apache.hugegraph.entity.auth.UserView; +import org.apache.hugegraph.structure.auth.Belong; +import org.apache.hugegraph.structure.auth.User; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +public class GraphSpaceUserService extends AuthService { + + @Autowired + private BelongService belongService; + + @Autowired + private RoleService roleService; + + public List listUsers(HugeClient client) { + List users = new ArrayList<>(); + List belongs = new ArrayList<>(); + this.roleService.list(client).forEach(role -> { + belongs.addAll(this.belongService.listByRole(client, + role.id().toString())); + }); + + Multimap grouped = ArrayListMultimap.create(); + belongs.forEach(belong -> { + grouped.put(belong.getUserId(), belong); + }); + + grouped.keySet().forEach(userId -> { + UserView user = new UserView(null, null, new ArrayList<>()); + grouped.get(userId).forEach(belong -> { + user.setId(belong.getUserId()); + user.setName(belong.getUserName()); + user.addRole(new RoleEntity(belong.getRoleId(), + belong.getRoleName())); + }); + users.add(user); + }); + return users; + } + + public UserView getUser(HugeClient client, String userId) { + List belongs = this.belongService.listByUser(client, + userId); + UserView user = new UserView(null, null, + new ArrayList<>(belongs.size())); + belongs.forEach(belong -> { + user.setId(belong.getUserId()); + user.setName(belong.getUserName()); + user.addRole(new RoleEntity(belong.getRoleId(), + belong.getRoleName())); + }); + return user; + } + + public IPage queryPage(HugeClient client, String query, + int pageNo, int pageSize) { + List results = + this.listUsers(client).stream() + .filter(user -> user.getName().contains(query)) + .sorted(Comparator.comparing(UserView::getName)) + .collect(Collectors.toList()); + return PageUtil.page(results, pageNo, pageSize); + } + + public UserView createOrUpdate(HugeClient client, UserView userView) { + E.checkNotNull(userView.getId(), "User Id Not Null"); + E.checkArgument(userView.getRoles() != null && + !userView.getRoles().isEmpty(), + "The role info is empty"); + + Set newRoles = + userView.getRoles().stream() + .map(RoleEntity::getId) + .collect(Collectors.toSet()); + this.belongService.listByUser(client, userView.getId()) + .forEach(belong -> { + if (!newRoles.contains(belong.getRoleId())) { + client.auth().deleteBelong(belong.getId()); + } + }); + + userView.getRoles().forEach(role -> { + if (!this.belongService.exists(client, role.getId(), + userView.getId())) { + Belong belong = new Belong(); + belong.user(userView.getId()); + belong.group(role.getId()); + this.belongService.add(client, belong); + } + }); + return this.getUser(client, userView.getId()); + } + + public void unauthUser(HugeClient client, String userId) { + List belongs = this.belongService.listByUser(client, + userId); + E.checkState(!belongs.isEmpty(), "The user: (%s) not exists", userId); + belongs.forEach(belong -> { + this.belongService.delete(client, belong.getId()); + }); + } + + public IPage querySpaceAdmins(HugeClient client, String graphSpace, + String query, int pageNo, + int pageSize) { + List spaceAdmins = + this.getSpaceAdmins(client, graphSpace).stream() + .filter(user -> user.name().contains(query)) + .sorted(Comparator.comparing(User::name)) + .collect(Collectors.toList()); + return PageUtil.page(spaceAdmins, pageNo, pageSize); + } + + private List getSpaceAdmins(HugeClient client, String graphSpace) { + List spaceAdmins = client.auth().listSpaceAdmin(graphSpace); + ArrayList users = new ArrayList<>(); + for (String spaceAdmin : spaceAdmins) { + users.add(client.auth().getUser(spaceAdmin)); + } + return users; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GroupService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GroupService.java new file mode 100644 index 000000000..9fb378a23 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GroupService.java @@ -0,0 +1,89 @@ +///* +// * +// * Licensed to the Apache Software Foundation (ASF) under one or more +// * contributor license agreements. See the NOTICE file distributed with this +// * work for additional information regarding copyright ownership. The ASF +// * licenses this file to You 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 org.apache.hugegraph.service.auth; +// +//import org.apache.hugegraph.driver.AuthManager; +//import org.apache.hugegraph.driver.HugeClient; +//import org.apache.hugegraph.exception.ExternalException; +//import org.apache.hugegraph.structure.auth.Group; +//import org.apache.hugegraph.util.PageUtil; +//import com.baomidou.mybatisplus.core.metadata.IPage; +//import lombok.extern.log4j.Log4j2; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.stereotype.Service; +// +//import java.util.ArrayList; +//import java.util.List; +//import java.util.Map; +// +//@Log4j2 +//@Service +//public class GroupService extends AuthService{ +// @Autowired +// UserService userService; +// public Group get(HugeClient client, String gid) { +// AuthManager auth = client.auth(); +// Group group = auth.getGroup(gid); +// if (group == null) { +// throw new ExternalException("auth.group.get.not-exist", +// gid); +// } +// return group; +// } +// +// public List list(HugeClient client) { +// List groups = client.auth().listGroups(); +// return groups; +// } +// +// public IPage queryPage(HugeClient client, String query, +// int pageNo, int pageSize) { +// ArrayList results = new ArrayList<>(); +// client.auth().listGroups().stream().filter((g) -> g.nickname().contains(query)) +// .forEach((g) -> { +// results.add(g); +// }); +// +// return PageUtil.page(results, pageNo, pageSize); +// } +// +// public Group update(HugeClient client, Group group) { +// AuthManager auth = client.auth(); +// if (auth.getGroup(group.id()) == null ) { +// throw new ExternalException("auth.group.not-exist", +// group.id(), group.name()); +// } +// return auth.updateGroup(group); +// } +// +// public Group insert(HugeClient client, Group group) { +// AuthManager auth = client.auth(); +// return auth.createGroup(group); +// } +// +// public void delete(HugeClient client, String gid) { +// AuthManager auth = client.auth(); +// auth.deleteGroup(gid); +// } +// +// public Map batch(HugeClient client, String gid , Map action) { +// AuthManager auth = client.auth(); +// return auth.batchGroup(gid, action); +// } +//} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/LoginAttemptGuard.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/LoginAttemptGuard.java new file mode 100644 index 000000000..2d34c01db --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/LoginAttemptGuard.java @@ -0,0 +1,164 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; + +import org.apache.hugegraph.exception.LoginThrottledException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Component +public class LoginAttemptGuard { + + private final Cache attempts; + private final LongSupplier clock; + private final int failureThreshold; + private final long initialBackoffMillis; + private final long maxBackoffMillis; + + @Autowired + public LoginAttemptGuard( + @Value("${auth.login.failure-threshold}") int failureThreshold, + @Value("${auth.login.initial-backoff-seconds}") long initialSeconds, + @Value("${auth.login.max-backoff-seconds}") long maxSeconds, + @Value("${auth.login.max-tracked-principals}") long maximumSize) { + this(failureThreshold, TimeUnit.SECONDS.toMillis(initialSeconds), + TimeUnit.SECONDS.toMillis(maxSeconds), maximumSize, + System::currentTimeMillis); + } + + public LoginAttemptGuard(int failureThreshold, long initialBackoffMillis, + long maxBackoffMillis, long maximumSize, + LongSupplier clock) { + if (failureThreshold < 1 || initialBackoffMillis < 1L || + maxBackoffMillis < initialBackoffMillis || maximumSize < 1L) { + throw new IllegalArgumentException("Invalid login backoff settings"); + } + this.clock = clock; + this.failureThreshold = failureThreshold; + this.initialBackoffMillis = initialBackoffMillis; + this.maxBackoffMillis = maxBackoffMillis; + // FIXME: For multi-instance or hostile high-cardinality traffic, move this + // state to a shared rate limiter that preserves active bans under key spray. + this.attempts = Caffeine.newBuilder() + .maximumSize(maximumSize) + .expireAfterAccess(maxBackoffMillis * 2L, + TimeUnit.MILLISECONDS) + .build(); + } + + public void checkAllowed(String username, String address) { + String key = key(username, address); + long now = this.clock.getAsLong(); + long[] retrySeconds = {0L}; + this.attempts.asMap().compute(key, (ignored, current) -> { + Attempt attempt = current == null ? Attempt.EMPTY : current; + if (attempt.blockedUntil > now) { + retrySeconds[0] = retrySeconds(attempt.blockedUntil - now); + return attempt; + } + if (attempt.inFlight) { + retrySeconds[0] = 1L; + return attempt; + } + return new Attempt(attempt.failures, attempt.blockedUntil, true); + }); + if (retrySeconds[0] > 0L) { + throw new LoginThrottledException(retrySeconds[0]); + } + } + + public void recordFailure(String username, String address) { + String key = key(username, address); + Attempt attempt = this.attempts.asMap().compute(key, (ignored, current) -> { + int failures = current == null ? 1 : increment(current.failures); + long blockedUntil = 0L; + if (failures >= this.failureThreshold) { + int exponent = failures - this.failureThreshold; + long delay = backoff(exponent, this.initialBackoffMillis, + this.maxBackoffMillis); + blockedUntil = this.clock.getAsLong() + delay; + } + return new Attempt(failures, blockedUntil, false); + }); + if (attempt.failures >= this.failureThreshold) { + log.warn("Login throttled for principal {}, failures={}, retry={}s", + Integer.toHexString(key.hashCode()), attempt.failures, + Math.max(1L, (attempt.blockedUntil - this.clock.getAsLong() + + 999L) / 1000L)); + } + } + + public void reset(String username, String address) { + this.attempts.invalidate(key(username, address)); + } + + public void release(String username, String address) { + this.attempts.asMap().computeIfPresent(key(username, address), + (ignored, current) -> + new Attempt(current.failures, + current.blockedUntil, + false)); + } + + private static String key(String username, String address) { + return String.valueOf(username) + '|' + + String.valueOf(address); + } + + private static int increment(int value) { + return value == Integer.MAX_VALUE ? value : value + 1; + } + + private static long retrySeconds(long remainingMillis) { + return Math.max(1L, (remainingMillis + 999L) / 1000L); + } + + private static long backoff(int exponent, long initial, long maximum) { + long delay = initial; + for (int i = 0; i < exponent && delay < maximum; i++) { + delay = delay > maximum / 2L ? maximum : delay * 2L; + } + return Math.min(delay, maximum); + } + + private static final class Attempt { + + private static final Attempt EMPTY = new Attempt(0, 0L, false); + + private final int failures; + private final long blockedUntil; + private final boolean inFlight; + + private Attempt(int failures, long blockedUntil, boolean inFlight) { + this.failures = failures; + this.blockedUntil = blockedUntil; + this.inFlight = inFlight; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/ManagerService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/ManagerService.java new file mode 100644 index 000000000..1c51b9bf7 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/ManagerService.java @@ -0,0 +1,31 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import org.apache.hugegraph.driver.HugeClient; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ManagerService extends AuthService { + public List getManager(HugeClient client) { + return null; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/RoleService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/RoleService.java new file mode 100644 index 000000000..71e30d3c6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/RoleService.java @@ -0,0 +1,140 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import java.util.ArrayList; +import java.util.List; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.driver.AuthManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.structure.auth.Group; +import org.apache.hugegraph.structure.auth.Role; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +public class RoleService extends AuthService { + + public Role get(HugeClient client, String roleId) { + return this.get(client, null, roleId); + } + + public Role get(HugeClient client, String graphSpace, String roleId) { + AuthManager auth = client.auth(); + Group group = auth.getGroup(roleId); + if (group == null) { + throw new ExternalException("auth.role.get.not-exist", roleId); + } + return toRole(graphSpace, group); + } + + public List list(HugeClient client) { + return this.list(client, null); + } + + public List list(HugeClient client, String graphSpace) { + List roles = new ArrayList<>(); + client.auth().listGroups().forEach(group -> { + roles.add(toRole(graphSpace, group)); + }); + return roles; + } + + public IPage queryPage(HugeClient client, String query, + int pageNo, int pageSize) { + return this.queryPage(client, null, query, pageNo, pageSize); + } + + public IPage queryPage(HugeClient client, String graphSpace, + String query, int pageNo, int pageSize) { + ArrayList results = new ArrayList<>(); + this.list(client, graphSpace).stream() + .filter(role -> role.nickname().contains(query)) + .forEach(results::add); + return PageUtil.page(results, pageNo, pageSize); + } + + public Role update(HugeClient client, Role role) { + AuthManager auth = client.auth(); + Group group = auth.getGroup(role.id()); + if (group == null) { + throw new ExternalException("auth.role.not-exist", role.id(), + role.name()); + } + group.name(firstNonNull(role.name(), role.nickname(), group.name())); + group.description(role.description()); + return toRole(role.graphSpace(), auth.updateGroup(group)); + } + + public Role insert(HugeClient client, Role role) { + Group group = new Group(); + group.name(firstNonNull(role.name(), role.nickname())); + group.description(role.description()); + return toRole(role.graphSpace(), client.auth().createGroup(group)); + } + + public void delete(HugeClient client, String roleId) { + AuthManager auth = client.auth(); + Group group = RoleService.getGroup(auth, roleId); + auth.listAccessesByGroup(group, -1).forEach(access -> { + auth.deleteAccess(access.id()); + }); + auth.listBelongsByGroup(group, -1).forEach(belong -> { + auth.deleteBelong(belong.id()); + }); + auth.deleteGroup(roleId); + } + + protected static Group getGroup(AuthManager auth, String roleId) { + Group group = auth.getGroup(roleId); + if (group == null) { + throw new ExternalException("auth.role.not-exist", roleId); + } + return group; + } + + protected static Role toRole(String graphSpace, Group group) { + Role role = new Role(); + role.setId(group.id()); + role.graphSpace(graphSpace); + role.name(group.name()); + role.nickname(group.name()); + role.description(group.description()); + return role; + } + + private static String firstNonNull(String value, String fallback) { + return value != null ? value : fallback; + } + + private static String firstNonNull(String value, String fallback, + String defaultValue) { + if (value != null) { + return value; + } + if (fallback != null) { + return fallback; + } + return defaultValue; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/TargetService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/TargetService.java new file mode 100644 index 000000000..fee098bc1 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/TargetService.java @@ -0,0 +1,71 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.structure.auth.Target; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +public class TargetService extends AuthService { + + public List list(HugeClient client) { + return client.auth().listTargets(); + } + + public IPage queryPage(HugeClient client, String query, + int pageNo, int pageSize) { + List results = + this.list(client).stream() + .filter(target -> target.name().toLowerCase() + .contains(query.toLowerCase())) + .sorted(Comparator.comparing(Target::name)) + .collect(Collectors.toList()); + return PageUtil.page(results, pageNo, pageSize); + } + + public Target get(HugeClient client, String targetId) { + Target target = client.auth().getTarget(targetId); + if (target == null) { + throw new ExternalException("auth.target.not-exist.id", targetId); + } + return target; + } + + public Target add(HugeClient client, Target target) { + return client.auth().createTarget(target); + } + + public Target update(HugeClient client, Target target) { + return client.auth().updateTarget(target); + } + + public void delete(HugeClient client, String targetId) { + client.auth().deleteTarget(targetId); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java new file mode 100644 index 000000000..8b67a8c9b --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java @@ -0,0 +1,545 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import java.io.File; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.structure.auth.Login; +import org.apache.hugegraph.options.HubbleOptions; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.AuthManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.UserEntity; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.structure.auth.User; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.PageUtil; + +import com.csvreader.CsvReader; + +import org.springframework.web.multipart.MultipartFile; + +@Log4j2 +@Service +public class UserService extends AuthService { + + public static final String CREATE_SUCCESS = "successfully created"; + + //@Autowired + //BelongService belongService; + + @Autowired + ManagerService managerService; + + @Autowired + private HugeConfig config; + + private boolean isPdEnabled() { + return config.get(HubbleOptions.PD_ENABLED); + } + + public List listUsers(HugeClient hugeClient) { + AuthManager auth = hugeClient.auth(); + + List users = auth.listUsers(); + List ues = new ArrayList<>(users.size()); + Map countMap = new HashMap<>(); + Map> spaceMap = new HashMap<>(); + users.forEach(u -> { + UserEntity ue = convert(hugeClient, u); + if (isPdEnabled()) { + ue.setSuperadmin(isSuperAdmin(hugeClient, ue.getId())); + } else { + ue.setSuperadmin(false); + } + ues.add(ue); + }); + if (isPdEnabled()) { + List listMap = getSpaceAndSpacenum(hugeClient); + spaceMap = HubbleUtil.uncheckedCast(listMap.get(0)); + countMap = HubbleUtil.uncheckedCast(listMap.get(1)); + for (UserEntity user : ues) { + user.setSpacenum(countMap.get(user.getName())); + user.setAdminSpaces(spaceMap.get(user.getName())); + } + } + + return ues; + } + + public UserEntity getUser(HugeClient client, String name) { + return convert(client, client.auth().getUserByName(name)); + } + + public Object queryPage(HugeClient hugeClient, String query, + int pageNo, int pageSize) { + AuthManager auth = hugeClient.auth(); + Map countMap = new HashMap<>(); + Map> spaceMap = new HashMap<>(); + + List results = + hugeClient.auth().listUsers().stream() + .filter((u) -> u.name().contains(query) || + u.nickname() != null && u.nickname().contains(query)) + .sorted(Comparator.comparing(User::name)) + .map((u) -> { + UserEntity ue = convert(hugeClient, u); + return ue; + }).collect(Collectors.toList()); + + if (isPdEnabled()) { + List listMap = getSpaceAndSpacenum(hugeClient); + spaceMap = HubbleUtil.uncheckedCast(listMap.get(0)); + countMap = HubbleUtil.uncheckedCast(listMap.get(1)); + for (UserEntity user : results) { + user.setSpacenum(countMap.get(user.getName())); + user.setAdminSpaces(spaceMap.get(user.getName())); + user.setSuperadmin(isSuperAdmin(hugeClient, user.getId())); + } + } + return PageUtil.page(results, pageNo, pageSize); + } + + public UserEntity get(HugeClient hugeClient, String userId) { + AuthManager auth = hugeClient.auth(); + User user = auth.getUser(userId); + if (user == null) { + throw new InternalException("auth.user.get.%s Not Exits", + userId); + } + UserEntity userEntity = convert(hugeClient, user); + if (isPdEnabled()) { + userEntity.setSuperadmin(isSuperAdmin(hugeClient, userEntity.getId())); + List spaces = hugeClient.graphSpace().listGraphSpace(); + List listMap = getSpaceAndSpacenum(hugeClient); + Map> spaceMap = + HubbleUtil.uncheckedCast(listMap.get(0)); + List adminSpaces = spaceMap.get(userId); + List resSpaces = new ArrayList<>(); + for (String space : spaces) { + if (hugeClient.graphSpace().checkDefaultRole(space, userId, "analyst")) { + resSpaces.add(space); + } + } + resSpaces.addAll(adminSpaces); + userEntity.setAdminSpaces(adminSpaces); + userEntity.setSpacenum(adminSpaces.size()); + userEntity.setResSpaces(resSpaces); + } else { + userEntity.setSuperadmin(false); + userEntity.setAdminSpaces(new ArrayList<>()); + userEntity.setSpacenum(0); + userEntity.setResSpaces(new ArrayList<>()); + } + return userEntity; + } + + public UserEntity getpersonal(HugeClient hugeClient, String username) { + AuthManager auth = hugeClient.auth(); + User user = auth.getUserByName(username); + if (user == null) { + throw new InternalException("auth.user.get.%s Not Exits", + username); + } + UserEntity userEntity = convert(hugeClient, user); + if (isPdEnabled()) { + userEntity.setSuperadmin(isSuperAdmin(hugeClient)); + List adminSpaces = new ArrayList<>(); + List resSpaces = new ArrayList<>(); + List spaces = hugeClient.graphSpace().listGraphSpace(); + for (String space : spaces) { + if (hugeClient.auth().isSpaceAdmin(space)) { + adminSpaces.add(space); + } + if (hugeClient.auth().isSpaceAdmin(space) || + hugeClient.auth().checkDefaultRole(space, "analyst")) { + resSpaces.add(space); + } + } + userEntity.setAdminSpaces(adminSpaces); + userEntity.setSpacenum(adminSpaces.size()); + userEntity.setResSpaces(resSpaces); + } else { + userEntity.setSuperadmin(false); + userEntity.setAdminSpaces(new ArrayList<>()); + userEntity.setSpacenum(0); + userEntity.setResSpaces(new ArrayList<>()); + } + return userEntity; + } + + public void add(HugeClient client, UserEntity ue) { + User user = new User(); + user.name(ue.getName()); + user.password(ue.getPassword()); + user.phone(ue.getPhone()); + user.email(ue.getEmail()); + user.avatar(ue.getAvatar()); + user.description(ue.getDescription()); + user.nickname(ue.getNickname()); + + User newUser = client.auth().createUser(user); + if (ue.getAdminSpaces() != null) { + for (String graphspace : ue.getAdminSpaces()) { + client.auth().addSpaceAdmin(ue.getName(), graphspace); + } + } + + if (newUser != null && ue.isSuperadmin()) { + // add superadmin + client.auth().addSuperAdmin(newUser.id().toString()); + } + } + + public String addbatch(HugeClient client, MultipartFile csvFile) { + File file = multipartFileToFile(csvFile); + try { + Map csv = readCsvByCsvReader(file); + List> createBatchBody = + HubbleUtil.uncheckedCast(csv.get("data")); + Map>> result = + client.auth().createBatch(createBatchBody); + List> resultList = result.get("result"); + List failedList = new ArrayList<>(createBatchBody.size()); + for (Map entry : resultList) { + if (!CREATE_SUCCESS.equals(entry.get("result"))) { + failedList.add(entry.get("user_name")); + } + } + if (!failedList.isEmpty()) { + throw new InternalException("auth.user.batch-create.failed", + failedList); + } + return "success"; + } finally { + if (!file.delete()) { + log.warn("Failed to delete temporary user import file '{}'; " + + "it will be removed by the operating system", file); + } + } + } + + public File multipartFileToFile(MultipartFile multiFile) { + File file = null; + try { + String originalName = multiFile.getOriginalFilename(); + String suffix = ".tmp"; + if (originalName != null) { + int dot = originalName.lastIndexOf('.'); + String candidate = dot >= 0 ? originalName.substring(dot) : ""; + if (candidate.matches("\\.[A-Za-z0-9]{1,16}")) { + suffix = candidate; + } + } + file = File.createTempFile("hubble-user-import-", suffix); + multiFile.transferTo(file); + return file; + } catch (Exception e) { + log.error("Failed to create a temporary file for user import", e); + if (file != null && file.exists() && !file.delete()) { + log.warn("Failed to delete incomplete user import file '{}'", + file); + } + throw new InternalException("auth.user.import-file.failed", e); + } + } + + public Map readCsvByCsvReader(File file) { + if (file == null) { + throw new InternalException("auth.user.import-file.failed"); + } + Map mapData = new HashMap<>(); + String fileName = file.getName(); + mapData.put("sheetName", fileName); + + List strList = new ArrayList<>(); + List> list = new ArrayList<>(); + try { + List arrList = new ArrayList(); + CsvReader reader = new CsvReader(file.getPath(), ',', Charset.forName("UTF-8")); + // 读取表头 + reader.readHeaders(); + String[] headArray = reader.getHeaders(); + while (reader.readRecord()) { + // 按行读取,并把每一行的数据添加到list集合 + arrList.add(reader.getValues()); + } + reader.close(); + // 如果要返回 String[] 类型的 list 集合,则直接返回 arrList + // 以下步骤是把 String[] 类型的 list 集合转化为 String 类型的 list 集合 + for (int i = 0; i < arrList.size(); i++) { + // 组装String字符串 + // 如果不知道有多少列,则可再加一个循环 + Map map = new HashMap<>(); + for (int j = 0; j < arrList.get(0).length; j++) { + map.put("" + headArray[j] + "", arrList.get(i)[j]); + } + list.add(map); + } + } catch (Exception e) { + log.error("Failed to parse the user import CSV file", e); + throw new InternalException("auth.user.import-csv.failed", e); + } + mapData.put("data", list); + return mapData; + } + + public void delete(HugeClient hugeClient, String userId) { + hugeClient.auth().deleteUser(userId); + } + + protected UserEntity convert(HugeClient client, User user) { + if (user == null) { + return null; + } + + UserEntity u = new UserEntity(); + u.setId(user.id().toString()); + u.setName(user.name()); + u.setNickname(user.nickname()); + u.setEmail(user.email()); + u.setPhone(user.phone()); + u.setAvatar(user.avatar()); + u.setDescription(user.description()); + u.setCreate(user.createTime()); + u.setUpdate(user.updateTime()); + u.setCreator(user.creator()); + + return u; + } + + protected List getSpaceAndSpacenum(HugeClient hugeClient) { + AuthManager auth = hugeClient.auth(); + List listMap = new ArrayList<>(); + if (!isPdEnabled()) { + // Non-PD mode: no GraphSpace/Manager APIs available + listMap.add(new HashMap<>()); + listMap.add(new HashMap<>()); + return listMap; + } + List users = auth.listUsers(); + List spaces = hugeClient.graphSpace().listGraphSpace(); + Map countMap = new HashMap<>(); + Map> spaceMap = new HashMap<>(); + for (User user : users) { + countMap.put(user.name(), 0); + spaceMap.put(user.name(), new ArrayList<>()); + } + + for (String space : spaces) { + List spaceManagers = + hugeClient.auth().listSpaceAdmin(space); + for (String spaceManager : spaceManagers) { + countMap.put(spaceManager, countMap.get(spaceManager) + 1); + List tempspace = spaceMap.get(spaceManager); + tempspace.add(space); + } + } + listMap.add(spaceMap); + listMap.add(countMap); + return listMap; + } + + public void update(HugeClient hugeClient, UserEntity userEntity) { + User user = new User(); + user.setId(userEntity.getId()); + user.name(userEntity.getName()); + user.password(userEntity.getPassword()); + user.phone(userEntity.getPhone()); + user.email(userEntity.getEmail()); + user.description(userEntity.getDescription()); + user.nickname(userEntity.getNickname()); + updateAdminSpace(hugeClient, userEntity.getName(), userEntity.getAdminSpaces()); + + // 设置超级管理员权限 + boolean curSuperAdmin = isSuperAdmin(hugeClient, user.id().toString()); + if (curSuperAdmin && !userEntity.isSuperadmin()) { + hugeClient.auth().delSuperAdmin(user.id().toString()); + } + if (!curSuperAdmin && userEntity.isSuperadmin()) { + hugeClient.auth().addSuperAdmin(user.id().toString()); + } + + hugeClient.auth().updateUser(user); + } + + public void updatePersonal(HugeClient hugeClient, String username, + String nickname, String description) { + AuthManager auth = hugeClient.auth(); + User user = auth.getUserByName(username); + user.nickname(nickname); + user.description(description); + user.password(null); + hugeClient.auth().updateUser(user); + } + + public Response updatepwd(HugeClient hugeClient, String username, + String oldpwd, String newpwd) { + Login login = new Login(); + login.name(username); + login.password(oldpwd); + try { + hugeClient.auth().login(login); + } catch (Exception e) { + return Response.builder() + .status(Constant.STATUS_BAD_REQUEST) + .message(e.getMessage()) + .cause(null) + .build(); + } + // Must fetch user first to get the ID, otherwise updateUser sends + // PUT to the collection path (no {id}) and gets HTTP 405. + User user = hugeClient.auth().getUserByName(username); + user.password(newpwd); + hugeClient.auth().updateUser(user); + return Response.builder() + .status(Constant.STATUS_OK) + .build(); + } + + public List listAdminSpace(HugeClient hugeClient, String username) { + if (!isPdEnabled()) { + return new ArrayList<>(); + } + AuthManager auth = hugeClient.auth(); + List users = auth.listUsers(); + List spaces = hugeClient.graphSpace().listGraphSpace(); + List adminspace = new ArrayList(); + for (String space : spaces) { + List spaceManagers = + hugeClient.auth().listSpaceAdmin(space); + for (String spaceManager : spaceManagers) { + if (spaceManager.equals(username)) { + adminspace.add(space); + } + } + } + return adminspace; + } + + public void updateAdminSpace(HugeClient hugeClient, String username, + List adminspaces) { + if (adminspaces == null || !isPdEnabled()) { + return; + } + List oldadminspaces = listAdminSpace(hugeClient, username); + for (String adminspace : adminspaces) { + if (!oldadminspaces.contains(adminspace)) { + hugeClient.auth().addSpaceAdmin(username, adminspace); + } + } + for (String oldadminspace : oldadminspaces) { + if (!adminspaces.contains(oldadminspace)) { + hugeClient.auth().delSpaceAdmin(username, oldadminspace); + } + } + } + + public String userLevel(HugeClient client) { + if (!isPdEnabled()) { + // In non-PD mode, Manager/GraphSpace APIs are not available. + // Treat the logged-in user as ADMIN for full access. + return "ADMIN"; + } + + if (isSuperAdmin(client)) { + return "ADMIN"; + } + + if (isSpaceAdmin(client)) { + return "SPACEADMIN"; + } + + // Default: user + return "USER"; + } + + public boolean isSuperAdmin(HugeClient client, String uid) { + if (!isPdEnabled()) { + return false; + } + // Only used by superadmin + // Check: if user is spaceadmin for any graphspace + return client.auth().listSuperAdmin().contains(uid); + } + + public boolean isSuperAdmin(HugeClient client) { + if (!isPdEnabled()) { + return false; + } + // Check: if current user is superadmin + return client.auth().isSuperAdmin(); + } + + /* + public boolean isAssignSpaceAdmin(HugeClient client, String uid, + String graphSpace) { + // Only used by superadmin + // Check: if user is spaceadmin for one graphSpace + return client.auth().listSpaceAdmin(graphSpace).contains(uid); + } + */ + + public boolean isAssignSpaceAdmin(HugeClient client, String graphSpace) { + if (!isPdEnabled()) { + return false; + } + // Check: if current user is spaceadmin + return client.auth().isSpaceAdmin(graphSpace); + } + + public boolean isSpaceAdmin(HugeClient client) { + if (!isPdEnabled()) { + return false; + } + // Check: if current user is spaceadmin + List graphSpaces = client.graphSpace().listGraphSpace(); + for (String gs : graphSpaces) { + if (isAssignSpaceAdmin(client, gs)) { + return true; + } + } + + return false; + } + + // List graphspace admin + public List listGraphSpaceAdmin(HugeClient client, + String graphSpace) { + if (!isPdEnabled()) { + return new ArrayList<>(); + } + AuthManager auth = client.auth(); + + return auth.listSpaceAdmin(graphSpace); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/WhiteIpListService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/WhiteIpListService.java new file mode 100644 index 000000000..44784c697 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/WhiteIpListService.java @@ -0,0 +1,53 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.auth; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.WhiteIpListManager; +import org.apache.hugegraph.entity.auth.WhiteIpListEntity; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Log4j2 +@Service +public class WhiteIpListService extends AuthService { + public Map get(HugeClient client) { + WhiteIpListManager whiteIpListManager = client.whiteIpListManager(); + Map whiteIpList = whiteIpListManager.list(); + return whiteIpList; + } + + public Map batch(HugeClient client, WhiteIpListEntity whiteIpListEntity) { + WhiteIpListManager whiteIpListManager = client.whiteIpListManager(); + Map actionMap = new HashMap<>(); + actionMap.put("action", whiteIpListEntity.getAction()); + actionMap.put("ips", whiteIpListEntity.getIps()); + Map whiteIpList = whiteIpListManager.batch(actionMap); + return whiteIpList; + } + + public Map updatestatus(HugeClient client, boolean status) { + WhiteIpListManager whiteIpListManager = client.whiteIpListManager(); + Map whiteIpList = whiteIpListManager.update(status); + return whiteIpList; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graph/GraphService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graph/GraphService.java index 8523e9e9e..89015f451 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graph/GraphService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graph/GraphService.java @@ -18,13 +18,16 @@ package org.apache.hugegraph.service.graph; -import java.util.Map; - +import com.google.common.collect.ImmutableSet; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.driver.GraphManager; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.graph.EdgeEntity; import org.apache.hugegraph.entity.graph.VertexEntity; +import org.apache.hugegraph.entity.graph.VertexQueryEntity; import org.apache.hugegraph.entity.query.GraphView; import org.apache.hugegraph.entity.schema.EdgeLabelEntity; import org.apache.hugegraph.entity.schema.PropertyKeyEntity; @@ -34,7 +37,9 @@ import org.apache.hugegraph.loader.source.file.FileSource; import org.apache.hugegraph.loader.source.file.ListFormat; import org.apache.hugegraph.loader.util.DataTypeUtil; -import org.apache.hugegraph.service.HugeClientPoolService; +import org.apache.hugegraph.loader.util.JsonUtil; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.auth.UserService; import org.apache.hugegraph.service.schema.EdgeLabelService; import org.apache.hugegraph.service.schema.PropertyKeyService; import org.apache.hugegraph.service.schema.VertexLabelService; @@ -44,19 +49,27 @@ import org.apache.hugegraph.structure.graph.Vertex; import org.apache.hugegraph.structure.schema.PropertyKey; import org.apache.hugegraph.util.Ex; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; -import com.google.common.collect.ImmutableSet; - -import lombok.extern.log4j.Log4j2; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; @Log4j2 @Service public class GraphService { - @Autowired - private HugeClientPoolService poolService; @Autowired private PropertyKeyService pkService; @Autowired @@ -64,76 +77,328 @@ public class GraphService { @Autowired private EdgeLabelService elService; - public HugeClient client(int connId) { - return this.poolService.getOrCreate(connId); - } + @Autowired + private UserService userService; + @Autowired + private HugeConfig config; - public GraphView addVertex(int connId, VertexEntity entity) { - HugeClient client = this.client(connId); - Vertex vertex = this.buildVertex(connId, entity); + public GraphView addVertex(HugeClient client, VertexEntity entity) { + this.checkParamsValid(client, entity, true); + Vertex vertex = this.buildVertex(client, entity); vertex = client.graph().addVertex(vertex); return GraphView.builder() - .vertices(ImmutableSet.of(vertex)) + .vertices(ImmutableSet.of( + VertexQueryEntity.fromVertex(vertex))) .edges(ImmutableSet.of()) .build(); } - public Vertex updateVertex(int connId, VertexEntity entity) { - HugeClient client = this.client(connId); + public Vertex updateVertex(HugeClient client, String vertexId, VertexEntity entity) { + this.checkParamsValid(client, entity, false); GraphManager graph = client.graph(); - Vertex vertex = this.buildVertex(connId, entity); - // TODO: client should add updateVertex() method - return graph.addVertex(vertex); + Vertex vertex = this.buildVertex(client, entity); + return graph.updateVertexProperty(vertexId, vertex); } - private Vertex buildVertex(int connId, VertexEntity entity) { + public void deleteVertex(HugeClient client, Object vertexId) { + client.graph().deleteVertex(vertexId); + } + + private Vertex buildVertex(HugeClient client, VertexEntity entity) { Vertex vertex = new Vertex(entity.getLabel()); - VertexLabelEntity vl = this.vlService.get(entity.getLabel(), connId); + VertexLabelEntity vl = this.vlService.get(entity.getLabel(), client); // Allowed front-end always pass id if (vl.getIdStrategy().isCustomize()) { Object vid = this.convertVertexId(vl.getIdStrategy(), entity.getId()); vertex.id(vid); } - this.fillProperties(connId, vl, vertex, entity.getProperties()); + this.fillProperties(client, vl, vertex, entity.getProperties()); return vertex; } - public GraphView addEdge(int connId, EdgeEntity entity) { - HugeClient client = this.client(connId); + public GraphView addEdge(HugeClient client, EdgeEntity entity) { + this.checkParamsValid(client, entity, true); GraphManager graph = client.graph(); - EdgeHolder edgeHolder = this.buildEdge(connId, entity); + EdgeHolder edgeHolder = this.buildEdge(client, entity); Edge edge = graph.addEdge(edgeHolder.edge); Vertex source = edgeHolder.source; Vertex target = edgeHolder.target; return GraphView.builder() - .vertices(ImmutableSet.of(source, target)) + .vertices(ImmutableSet.of( + VertexQueryEntity.fromVertex(source), + VertexQueryEntity.fromVertex(target))) .edges(ImmutableSet.of(edge)) .build(); } - public Edge updateEdge(int connId, EdgeEntity entity) { - HugeClient client = this.client(connId); + public void deleteEdge(HugeClient client, String edgeId) { + client.graph().deleteEdge(edgeId); + } + + public Edge updateEdge(HugeClient client, String edgeId, EdgeEntity entity) { + this.checkParamsValid(client, entity, false); + GraphManager graph = client.graph(); + EdgeHolder edgeHolder = this.buildEdge(client, entity); + return graph.updateEdgeProperty(edgeId, edgeHolder.edge); + } + + public HashMap getVertexProperties(HugeClient client, String label) { + VertexLabelEntity vlEntity = this.vlService.get(label, + client); + HashMap vertexPropertiesMap = new HashMap<>(); + vertexPropertiesMap.put("nonNullableProps", vlEntity.getNonNullableProps()); + vertexPropertiesMap.put("nullableProps", vlEntity.getNullableProps()); + vertexPropertiesMap.put("primaryKeys", vlEntity.getPrimaryKeys()); + return vertexPropertiesMap; + } + + public HashMap getEdgeProperties(HugeClient client, String label) { + EdgeLabelEntity elEntity = this.elService.get(label, + client); + HashMap edgePropertiesMap = new HashMap<>(); + edgePropertiesMap.put("nonNullableProps", elEntity.getNonNullableProps()); + edgePropertiesMap.put("nullableProps", elEntity.getNullableProps()); + edgePropertiesMap.put("sortKeys", elEntity.getSortKeys()); + return edgePropertiesMap; + } + + public HashMap getVertexStyle(HugeClient client, List labels) { + HashMap vertexStyles = new HashMap<>(); + for (String label : labels) { + VertexLabelEntity vlEntity = this.vlService.get(label, + client); + vertexStyles.put(label, vlEntity.getStyle()); + } + return vertexStyles; + } + + public HashMap getEdgeStyle(HugeClient client, List labels) { + HashMap edgeStyles = new HashMap<>(); + for (String label : labels) { + EdgeLabelEntity elEntity = this.elService.get(label, + client); + edgeStyles.put(label, elEntity.getStyle()); + } + return edgeStyles; + } + + public GraphView importJson(HugeClient client, MultipartFile jsonFile) throws IOException { + this.checkImportFile(jsonFile); + File file = userService.multipartFileToFile(jsonFile); + String content = FileUtils.readFileToString(file, + StandardCharsets.UTF_8); + JSONObject jsonObject; + try { + jsonObject = new JSONObject(content); + } catch (JSONException e) { + throw new ExternalException("graph.import.invalid-json", e); + } + JSONArray verticesArray = this.getImportArray(jsonObject, "vertices"); + JSONArray edgesArray = this.getImportArray(jsonObject, "edges"); + List vertexEntities = + JsonUtil.convertList(verticesArray.toString(), + VertexEntity.class); + List edgeEntities = + JsonUtil.convertList(edgesArray.toString(), EdgeEntity.class); + + List vertices = new ArrayList<>(vertexEntities.size()); + List vertexImportIds = new ArrayList<>(vertexEntities.size()); + Map pendingVertices = new HashMap<>(); + for (VertexEntity entity : vertexEntities) { + this.checkParamsValid(client, entity, true); + Vertex vertex = this.buildVertex(client, entity); + Object importId = this.importVertexId(client, entity, vertex); + if (importId != null) { + Ex.check(!pendingVertices.containsKey(importId), + "graph.import.vertex.duplicate-id", importId); + pendingVertices.put(importId, vertex); + } + vertices.add(vertex); + vertexImportIds.add(importId); + } + + Map edges = new HashMap<>(); + for (EdgeEntity entity : edgeEntities) { + entity.setId(null); + this.checkParamsValid(client, entity, true); + this.buildEdge(client, entity, pendingVertices); + } + + List addedVertexIds = new ArrayList<>(vertices.size()); + List addedEdgeIds = new ArrayList<>(edgeEntities.size()); GraphManager graph = client.graph(); - EdgeHolder edgeHolder = this.buildEdge(connId, entity); - // TODO: client should add updateEdge() - return graph.addEdge(edgeHolder.edge); + Map createdVertices = new HashMap<>(pendingVertices); + List createdVertexList = new ArrayList<>(vertices.size()); + try { + for (int i = 0; i < vertices.size(); i++) { + Vertex vertex = vertices.get(i); + Vertex created = graph.addVertex(vertex); + addedVertexIds.add(created.id()); + createdVertices.put(created.id(), created); + Object importId = vertexImportIds.get(i); + if (importId != null) { + createdVertices.put(importId, created); + } + createdVertexList.add(created); + } + for (EdgeEntity entity : edgeEntities) { + EdgeHolder edgeHolder = this.buildEdge(client, entity, + createdVertices); + Edge edge = graph.addEdge(edgeHolder.edge); + addedEdgeIds.add(edge.id()); + edges.put(edge.id(), edge); + } + } catch (RuntimeException e) { + this.rollbackImport(graph, addedVertexIds, addedEdgeIds); + throw e; + } + return GraphView.builder() + .vertices(VertexQueryEntity.fromVertices(createdVertexList)) + .edges(edges.values()) + .build(); + } + + private Object importVertexId(HugeClient client, VertexEntity entity, + Vertex vertex) { + if (vertex.id() != null) { + return vertex.id(); + } + VertexLabelEntity vl = this.vlService.get(entity.getLabel(), client); + if (vl.getIdStrategy().isPrimaryKey() && entity.getId() != null) { + return this.convertVertexId(vl.getIdStrategy(), entity.getId()); + } + return null; + } + + private void checkImportFile(MultipartFile jsonFile) { + Ex.check(jsonFile != null && !jsonFile.isEmpty(), + "common.param.cannot-be-null-or-empty", "file"); + long limit = this.importFileSizeLimit(); + Ex.check(jsonFile.getSize() <= limit, + "graph.import.file.exceed-limit", jsonFile.getSize(), limit); + } + + private long importFileSizeLimit() { + if (this.config == null) { + return HubbleOptions.UPLOAD_SINGLE_FILE_SIZE_LIMIT.defaultValue(); + } + return this.config.get(HubbleOptions.UPLOAD_SINGLE_FILE_SIZE_LIMIT); + } + + private JSONArray getImportArray(JSONObject jsonObject, String name) { + Ex.check(jsonObject.has(name), "graph.import.missing-field", name); + Object value = jsonObject.get(name); + Ex.check(value instanceof JSONArray, "graph.import.field-should-array", + name); + return (JSONArray) value; + } + + private void rollbackImport(GraphManager graph, List vertexIds, + List edgeIds) { + Collections.reverse(edgeIds); + for (String edgeId : edgeIds) { + try { + graph.deleteEdge(edgeId); + } catch (RuntimeException e) { + log.warn("Failed to rollback imported edge {}", edgeId, e); + } + } + Collections.reverse(vertexIds); + for (Object vertexId : vertexIds) { + try { + graph.deleteVertex(vertexId); + } catch (RuntimeException e) { + log.warn("Failed to rollback imported vertex {}", vertexId, e); + } + } } - private EdgeHolder buildEdge(int connId, EdgeEntity entity) { - HugeClient client = this.client(connId); + private void checkParamsValid(HugeClient client, VertexEntity entity, + boolean create) { + Ex.check(!StringUtils.isEmpty(entity.getLabel()), + "common.param.cannot-be-null-or-empty", "label"); + // If schema doesn't exist, it will throw exception + VertexLabelEntity vlEntity = this.vlService.get(entity.getLabel(), + client); + IdStrategy idStrategy = vlEntity.getIdStrategy(); + if (create) { + Ex.check(idStrategy.isCustomize(), () -> entity.getId() != null, + "common.param.cannot-be-null", "id"); + } else { + Ex.check(entity.getId() != null, + "common.param.cannot-be-null", "id"); + } + + Set nonNullableProps = vlEntity.getNonNullableProps(); + Map properties = entity.getProperties(); + if (properties == null) { + properties = Collections.emptyMap(); + entity.setProperties(properties); + } + if (create) { + Ex.check(properties.keySet().containsAll(nonNullableProps), + "graph.vertex.all-nonnullable-prop.should-be-setted"); + } + } + + private void checkParamsValid(HugeClient client, EdgeEntity entity, + boolean create) { + Ex.check(!StringUtils.isEmpty(entity.getLabel()), + "common.param.cannot-be-null-or-empty", "label"); + // If schema doesn't exist, it will throw exception + EdgeLabelEntity elEntity = this.elService.get(entity.getLabel(), client); + if (create) { + Ex.check(entity.getId() == null, + "common.param.must-be-null", "id"); + } else { + Ex.check(entity.getId() != null, + "common.param.cannot-be-null", "id"); + } + Ex.check(entity.getSourceId() != null, + "common.param.cannot-be-null", "source_id"); + Ex.check(entity.getTargetId() != null, + "common.param.cannot-be-null", "target_id"); + + Set nonNullableProps = elEntity.getNonNullableProps(); + Map properties = entity.getProperties(); + if (properties == null) { + properties = Collections.emptyMap(); + entity.setProperties(properties); + } + if (create) { + Ex.check(properties.keySet().containsAll(nonNullableProps), + "graph.edge.all-nonnullable-prop.should-be-setted"); + } + } + + private EdgeHolder buildEdge(HugeClient client, EdgeEntity entity) { + return this.buildEdge(client, entity, Collections.emptyMap()); + } + + private EdgeHolder buildEdge(HugeClient client, EdgeEntity entity, + Map pendingVertices) { GraphManager graph = client.graph(); - EdgeLabelEntity el = this.elService.get(entity.getLabel(), connId); + EdgeLabelEntity el = this.elService.get(entity.getLabel(), client); VertexLabelEntity sourceVl = this.vlService.get(el.getSourceLabel(), - connId); + client); VertexLabelEntity targetVl = this.vlService.get(el.getTargetLabel(), - connId); + client); Object realSourceId = this.convertVertexId(sourceVl.getIdStrategy(), entity.getSourceId()); Object realTargetId = this.convertVertexId(targetVl.getIdStrategy(), entity.getTargetId()); - Vertex sourceVertex = graph.getVertex(realSourceId); - Vertex targetVertex = graph.getVertex(realTargetId); + Vertex sourceVertex = pendingVertices.get(realSourceId); + if (sourceVertex == null) { + sourceVertex = graph.getVertex(realSourceId); + } + Vertex targetVertex = pendingVertices.get(realTargetId); + if (targetVertex == null) { + targetVertex = graph.getVertex(realTargetId); + } + + Ex.check(sourceVertex != null && targetVertex != null, + "gremlin.edges.linked-vertex.not-exist"); Ex.check(el.getSourceLabel().equals(sourceVertex.label()) && el.getTargetLabel().equals(targetVertex.label()), @@ -144,7 +409,7 @@ private EdgeHolder buildEdge(int connId, EdgeEntity entity) { Edge edge = new Edge(entity.getLabel()); edge.source(sourceVertex); edge.target(targetVertex); - this.fillProperties(connId, el, edge, entity.getProperties()); + this.fillProperties(client, el, edge, entity.getProperties()); return new EdgeHolder(edge, sourceVertex, targetVertex); } @@ -153,16 +418,16 @@ private Object convertVertexId(IdStrategy idStrategy, String rawId) { return rawId; } else if (idStrategy.isCustomizeNumber()) { return DataTypeUtil.parseNumber("id", rawId); - } else { - assert idStrategy.isCustomizeUuid(); + } else if (idStrategy.isCustomizeUuid()) { return DataTypeUtil.parseUUID("id", rawId); } + Ex.check(false, "Unsupported vertex id strategy: %s", idStrategy); + return rawId; } - private void fillProperties(int connId, SchemaLabelEntity schema, + private void fillProperties(HugeClient client, SchemaLabelEntity schema, GraphElement element, Map properties) { - HugeClient client = this.client(connId); for (Map.Entry entry : properties.entrySet()) { String key = entry.getKey(); Object rawValue = entry.getValue(); @@ -173,10 +438,11 @@ private void fillProperties(int connId, SchemaLabelEntity schema, continue; } } - PropertyKeyEntity pkEntity = this.pkService.get(key, connId); + PropertyKeyEntity pkEntity = this.pkService.get(key, client); PropertyKey propertyKey = PropertyKeyService.convert(pkEntity, client); - assert propertyKey != null; + Ex.check(propertyKey != null, "The property key '%s' is not found", + key); Object value; try { // DataTypeUtil.convert in loader need param InputSource diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java new file mode 100644 index 000000000..e17146804 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java @@ -0,0 +1,698 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.graphs; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.time.StopWatch; +import org.apache.hugegraph.client.RestClient; +import org.apache.hugegraph.api.graph.GraphMetricsAPI; +// TODO fix import +//import org.apache.hugegraph.client.api.graph.GraphMetricsAPI; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.query.GremlinController; +import org.apache.hugegraph.driver.GraphsManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.GraphConnection; +import org.apache.hugegraph.entity.enums.AsyncTaskStatus; +import org.apache.hugegraph.entity.enums.ExecuteStatus; +import org.apache.hugegraph.entity.enums.ExecuteType; +import org.apache.hugegraph.entity.graphs.GraphStatisticsEntity; +import org.apache.hugegraph.entity.query.ExecuteHistory; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.entity.space.BuiltInEntity; +import org.apache.hugegraph.loader.util.JsonUtil; +import org.apache.hugegraph.service.algorithm.AsyncTaskService; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.service.load.LoadTaskService; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.service.schema.SchemaService; +import org.apache.hugegraph.structure.Task; +import org.apache.hugegraph.structure.constant.GraphReadMode; +import org.apache.hugegraph.structure.gremlin.ResultSet; +import org.apache.hugegraph.util.Ex; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.apache.hugegraph.util.GremlinUtil.GREMLIN_LOAD_HLM; + +@Log4j2 +@Service +public class GraphsService { + + @Autowired + private SchemaService schemaService; + + @Autowired + UserService userService; + @Autowired + private QueryService queryService; + @Autowired + private ExecuteHistoryService historyService; + @Autowired + private AsyncTaskService asyncTaskService; + @Autowired + private LoadTaskService loadTaskService; + @Autowired + private org.apache.hugegraph.config.HugeConfig config; + + private static final String GRAPH_STORAGE = "v1/graph/%s/%s/g"; + private static final String RUNNING_TASKS = "running_tasks"; + private static final String STATISTICS = "statistics"; + private static final String GREMLIN_STATISTICS_VERTEX = + "g.V().groupCount().by(label)"; + private static final String GREMLIN_STATISTICS_EDGE = + "g.E().groupCount().by(label)"; + private static final String GRAPH_HLM = "hlm"; + private static final String GRAPH_COVID19 = "covid19"; + + private final ConcurrentHashMap> graphStatistics = + new ConcurrentHashMap<>(); + + public Map get(HugeClient client, String graphSpace, + String graph, + Map vermeerInfo) { + // long storage = getStorage(pdClient, graphSpace, graph); + Map info = new HashMap<>(); + info.putAll(client.graphs().getGraph(graph)); + // info.put("storage", storage); + // Ensure nickname/graphspace fields exist for frontend display + info.putIfAbsent("nickname", graph); + info.putIfAbsent("graphspace", graphSpace); + if (vermeerInfo.size() != 0) { + info.put("status", vermeerInfo.get("status")); + String lastLoadTime = vermeerInfo.get("update_time"); + // todo date format + info.put("last_load_time", lastLoadTime); + } + return info; + } + + public IPage> queryPage(HugeClient client, + String graphSpace, String uid, + String query, String createTime, + int pageNo, int pageSize, + boolean isVermeerEnabled, + Map vermeerInfo) { + List> results = + sortedGraphsProfile(client, graphSpace, query, createTime, + isVermeerEnabled, vermeerInfo); + + for (Map result : results) { + String graph = result.get("name").toString(); + try { + result.put("schemaview", schemaService.getSchemaView( + client.assignGraph(graphSpace, graph))); + } catch (Exception e) { + log.warn("Failed to load the schema summary for graph '{}'", + graph, e); + } + } + + return PageUtil.page(results, pageNo, pageSize); + } + + public List> sortedGraphsProfile(HugeClient client, + String graphSpace, + String query, + String createTime, + boolean isVermeerEnabled, + Map vermeerInfo) { + // Get authorized graphs + List> graphs = client.graphs().listProfile(query); + log.info("Query all graphs in '{}' ", graphSpace); + for (Map info : graphs) { + String name = info.get("name").toString(); + // delete pd.peers info for security + info.put("pd.peers", ""); + if (vermeerInfo.containsKey(name)) { + Map brief = + HubbleUtil.uncheckedCast(vermeerInfo.get(name)); + info.put("status", brief.get("status").toString()); + info.put("last_load_time", + brief.get("last_load_time").toString()); + } else if (isVermeerEnabled) { + // default info for non-loaded graph + info.put("status", "created"); + info.put("last_load_time", ""); + } else { + info.put("status", ""); + info.put("last_load_time", ""); + } + + info.put("storage", info.get("data_size")); + // Ensure graphspace field is present for frontend navigation + info.putIfAbsent("graphspace", graphSpace); + info.putIfAbsent("graphspace_nickname", graphSpace); + info.put("statistic", evCount(client, graphSpace, name)); + } + + List> results = + graphs.stream() + .filter((s) -> { + Object createTimeVal = s.get("create_time"); + // In standalone mode, create_time may be absent + if (createTimeVal == null) { + return true; + } + return createTimeVal.toString() + .compareTo(createTime) > 0; + }) + .sorted((graph1, graph2) -> { + Object d1 = graph1.get("default"); + Object d2 = graph2.get("default"); + boolean default1 = d1 instanceof Boolean && (boolean) d1; + boolean default2 = d2 instanceof Boolean && (boolean) d2; + + if (default1 != default2) { + return Boolean.compare(default2, default1); + } else if (default1) { + Object t1 = graph1.get("default_update_time"); + Object t2 = graph2.get("default_update_time"); + if (t1 instanceof Long && t2 instanceof Long) { + return ((Long) t1).compareTo((Long) t2); + } + return 0; + } else { + String name1 = graph1.get("name").toString(); + String name2 = graph2.get("name").toString(); + return name1.compareTo(name2); + } + }) + .collect(Collectors.toList()); + return results; + + } + + public Set listGraphNames(HugeClient client, String graphSpace, + String uid) { + + return ImmutableSet.copyOf(client.graphs().listGraph()); + } + + @Deprecated + public Map create(HugeClient client, String graph, + boolean isAuth, String schemaTemplate) { + Map conf = new HashMap<>(); + if (isAuth) { + conf.put("gremlin.graph", + "org.apache.hugegraph.auth.HugeFactoryAuthProxy"); + + } else { + conf.put("gremlin.graph", "org.apache.hugegraph.HugeFactory"); + } + if (!StringUtils.isEmpty(schemaTemplate)) { + conf.put("schema.init_template", schemaTemplate); + } + + conf.put("store", graph); + boolean pdEnabled = config.get(org.apache.hugegraph.options.HubbleOptions.PD_ENABLED); + if (pdEnabled) { + conf.put("backend", "hstore"); + } else { + conf.put("backend", "rocksdb"); + } + conf.put("serializer", "binary"); + + return client.graphs().createGraph(graph, JsonUtil.toJson(conf)); + } + + public Map create(HugeClient client, String nickname, + String graph, String schemaTemplate) { + Map conf = new HashMap<>(); + + conf.put("store", graph); + boolean pdEnabled = config.get(org.apache.hugegraph.options.HubbleOptions.PD_ENABLED); + if (pdEnabled) { + conf.put("backend", "hstore"); + conf.put("task.scheduler_type", "distributed"); + } else { + conf.put("backend", "rocksdb"); + conf.put("task.scheduler_type", "local"); + } + conf.put("serializer", "binary"); + conf.put("nickname", nickname); + + if (StringUtils.isNotEmpty(schemaTemplate)) { + conf.put("schema.init_template", schemaTemplate); + } + + return client.graphs().createGraph(graph, JsonUtil.toJson(conf)); + } + + public void update(HugeClient client, String nickname, + String graph) { + client.graphs().update(graph, nickname); + } + + public void clearGraph(HugeClient client, String graph) { + GraphsManager graphs = client.graphs(); + Map response = graphs.getDefault(); + Set defaults = defaultGraphs(response.get("default_graph")); + Ex.check(!defaults.contains(graph), + "The default graph can't be cleared"); + graphs.clearGraph(graph, "I'm sure to delete all data"); + } + + public void setDefault(HugeClient client, String graph) { + Map response = client.graphs().getDefault(); + Set defaults = defaultGraphs(response.get("default_graph")); + + boolean targetIsDefault = defaults.contains(graph); + if (!targetIsDefault) { + client.graphs().setDefault(graph); + } + for (String current : defaults) { + if (!graph.equals(current)) { + client.graphs().unSetDefault(current); + } + } + } + + private static Set defaultGraphs(Object value) { + if (value == null) { + return Collections.emptySet(); + } + + Collection values; + if (value instanceof Collection) { + values = (Collection) value; + } else if (value instanceof String) { + values = Collections.singleton(value); + } else { + throw new IllegalStateException("Invalid default_graph response type"); + } + + Set defaults = new LinkedHashSet<>(); + for (Object item : values) { + if (!(item instanceof String) || StringUtils.isBlank((String) item)) { + throw new IllegalStateException("Invalid default_graph entry"); + } + defaults.add((String) item); + } + return defaults; + } + + public void unSetDefault(HugeClient client, String graph) { + client.graphs().unSetDefault(graph); + } + + public Map getDefault(HugeClient client) { + return client.graphs().getDefault(); + } + + public void delete(HugeClient client, String graph, String confirmMessage) { + // TODO check if frontend support passing confirm message. + client.graphs().dropGraph(graph,confirmMessage); + } + + public GraphReadMode graphReadMode(HugeClient client, String graph) { + return client.graphs().readMode(graph); + } + + public void graphReadMode(HugeClient client, String graph, String mode) { + this.checkReadMode(mode); + // open(0) means mode equal all, close(1) means mode equal OLTP_ONLY + if ("0".equals(mode)) { + client.graphs().readMode(graph, GraphReadMode.ALL); + } else if ("1".equals(mode)) { + client.graphs().readMode(graph, GraphReadMode.OLTP_ONLY); + } + } + + public void checkReadMode(String mode) { + Ex.check("0".equals(mode) || "1".equals(mode), + "common.read_mode.invalid", mode); + } + + public Object clone(HugeClient client, Map params) { + return ImmutableMap.of("task_id", + client.graphs().clone(client.getGraphName(), + params)); + } + + public static long getStorage(RestClient pdClient, + String graphSpace, + String graph) { + if (pdClient == null) { + return 0L; + } + String path = String.format("v1/graph/%s/%s/g", graphSpace, graph); + Map result; + try { + result = HubbleUtil.uncheckedCast( + pdClient.get(path).readObject(Map.class)); + Map data = + HubbleUtil.uncheckedCast(result.get("data")); + if (data.containsKey("dataSize")) { + long dataSize = Long.valueOf(data.get("dataSize").toString()); + return dataSize; + } else { + return 0L; + } + } catch (Exception e) { + log.info("Fail to request pd to get data of graph {}-{} : {}", + graphSpace, graph, e.getMessage()); + return -1L; + } + } + + public static boolean isBigGraph(RestClient pdClient, String graphSpace, + String graph) { + return isBigStorage(getStorage(pdClient, graphSpace, graph)); + } + + public static boolean isBigStorage(long storageKb) { + return (storageKb > (2 * 1024 * 1024)); + } + + public static String getStatisticsKey(String graphSpace, String graph) { + return graphSpace + "-" + graph; + } + + public GraphStatisticsEntity getStatistics(HugeClient client, + String graphSpace, + String graph) { + GraphStatisticsEntity result; + this.graphStatistics.clear(); + result = postSmallStatistics(client, graphSpace, graph); + + return result; + } + + public void postStatistics(RestClient pdClient, + HugeClient client, + String graphSpace, + String graph) { + if (isBigGraph(pdClient, graphSpace, graph)) { + GremlinQuery query = new GremlinQuery(GREMLIN_STATISTICS_VERTEX); + long vid = executeAsyncTask(client, graphSpace, graph, query); + query.setContent(GREMLIN_STATISTICS_EDGE); + long eid = executeAsyncTask(client, graphSpace, graph, query); + String idPair = String.valueOf(vid) + "-" + String.valueOf(eid); + String graphKey = getStatisticsKey(graphSpace, graph); + if (this.graphStatistics.containsKey(graphKey)) { + Map graphCache = + this.graphStatistics.get(graphKey); + if (graphCache.get(RUNNING_TASKS) != null) { + List idPairs = + HubbleUtil.uncheckedCast(graphCache.get(RUNNING_TASKS)); + idPairs.add(idPair); + return; + } + } + List idPairs = new ArrayList<>(); + idPairs.add(idPair); + Map graphCache = new HashMap<>(2); + graphCache.put(RUNNING_TASKS, idPairs); + graphCache.put(STATISTICS, GraphStatisticsEntity.emptyEntity()); + this.graphStatistics.put(graphKey, graphCache); + } + } + + public GraphStatisticsEntity getLastStatistics(HugeClient client, + String graphSpace, + String graph) { + // used for big graph + String graphKey = getStatisticsKey(graphSpace, graph); + if (!this.graphStatistics.containsKey(graphKey)) { + // check graph statistics for the first time + return GraphStatisticsEntity.emptyEntity(); + } + + Map graphCache = this.graphStatistics.get(graphKey); + if (graphCache.get(RUNNING_TASKS) != null) { + List idPairs = + HubbleUtil.uncheckedCast(graphCache.get(RUNNING_TASKS)); + List idList = new ArrayList<>(idPairs.size() * 2); + for (String idPair: idPairs) { + String[] idVE = idPair.split("-"); + idList.add(Long.valueOf(idVE[0])); + idList.add(Long.valueOf(idVE[1])); + } + List tasks = asyncTaskService.list(client, idList); + idList.clear(); + + Map taskMap = new HashMap<>(tasks.size()); + for (Task task: tasks) { + taskMap.put(String.valueOf(task.id()), task); + } + + List removeIds = new ArrayList<>(); + Task lastV = null; + Task lastE = null; + boolean init = true; + for (String idPair: idPairs) { + String[] idVE = idPair.split("-"); + Task taskV = taskMap.get(idVE[0]); + Task taskE = taskMap.get(idVE[1]); + boolean success = taskV.success() && taskE.success(); + if (removable(taskV) || removable(taskE) || success) { + removeIds.add(idPair); + } + + if (success) { + // try to find last updated task + if (init) { + lastV = taskV; + lastE = taskE; + init = false; + } + if (lastV.updateTime() <= taskV.updateTime() && + lastE.updateTime() <= taskE.updateTime()) { + lastV = taskV; + lastE = taskE; + } + } + } + + idPairs.removeAll(removeIds); + removeIds.clear(); + taskMap.clear(); + + GraphStatisticsEntity result; + if (!init) { + result = updateCacheFromTask(client, lastV, lastE); + } else { + result = GraphStatisticsEntity.emptyEntity(); + } + graphCache.put(STATISTICS, result); + return result; + } else if (graphCache.get(STATISTICS) != null) { + return (GraphStatisticsEntity) graphCache.get(STATISTICS); + } else { + GraphStatisticsEntity result = GraphStatisticsEntity.emptyEntity(); + graphCache.put(STATISTICS, result); + return result; + } + } + + public static boolean removable(Task task) { + return task.completed() && !task.success(); + } + + public GraphStatisticsEntity updateCacheFromTask(HugeClient client, + Task taskV, Task taskE) { + GraphStatisticsEntity result = new GraphStatisticsEntity(); + taskV = asyncTaskService.get(client, + Integer.valueOf( + String.valueOf(taskV.id()))); + List> results = HubbleUtil.uncheckedCast( + JsonUtil.fromJson(taskV.result().toString(), List.class)); + result.setVertices(results.get(0)); + result.setVertexCount(getCountFromLabels(results.get(0))); + + taskE = asyncTaskService.get(client, + Integer.valueOf( + String.valueOf(taskE.id()))); + results = HubbleUtil.uncheckedCast( + JsonUtil.fromJson(taskE.result().toString(), List.class)); + result.setEdges(results.get(0)); + result.setEdgeCount(getCountFromLabels(results.get(0))); + result.setUpdateTime(HubbleUtil.dateFormat()); + return result; + } + + public GraphStatisticsEntity postSmallStatistics(HugeClient client, + String graphSpace, + String graph) { + GraphStatisticsEntity result = GraphStatisticsEntity.emptyEntity(); + ResultSet vertexResult = + queryService.executeQueryCount(client, + GREMLIN_STATISTICS_VERTEX); + ResultSet edgeResult = + queryService.executeQueryCount(client, + GREMLIN_STATISTICS_EDGE); + if (vertexResult.data() != null && vertexResult.data().size() != 0) { + Map vertices = + HubbleUtil.uncheckedCast(vertexResult.data().get(0)); + result.setVertices(vertices); + result.setVertexCount(getCountFromLabels(vertices)); + } + if (edgeResult.data() != null && edgeResult.data().size() != 0) { + Map edges = + HubbleUtil.uncheckedCast(edgeResult.data().get(0)); + result.setEdges(edges); + result.setEdgeCount(getCountFromLabels(edges)); + } + result.setUpdateTime(HubbleUtil.dateFormat()); + return result; + } + + public String getCountFromLabels(Map labels) { + Integer count = 0; + for (Map.Entry entry: labels.entrySet()) { + count += (Integer) entry.getValue(); + } + return count.toString(); + } + + public long executeAsyncTask(HugeClient client, String graphSpace, + String graph, GremlinQuery query) { + this.checkParamsValid(query); + + Date createTime = HubbleUtil.nowDate(); + // Insert execute history + ExecuteStatus status = ExecuteStatus.ASYNC_TASK_RUNNING; + ExecuteHistory history; + history = new ExecuteHistory(null, graphSpace, graph, 0L, + ExecuteType.GREMLIN_ASYNC, + query.getContent(), status, + AsyncTaskStatus.UNKNOWN, -1L, createTime); + this.historyService.save(history); + + StopWatch timer = StopWatch.createStarted(); + long asyncId = 0L; + try { + asyncId = this.queryService.executeGremlinAsyncTask(client, query); + status = ExecuteStatus.ASYNC_TASK_SUCCESS; + return asyncId; + } catch (Throwable e) { + status = ExecuteStatus.ASYNC_TASK_FAILED; + // TODO: Persist an async failure reason only after the Server Task + // DTO exposes a stable, sanitized reason code. Depending on task + // status alone cannot distinguish submission from execution + // failures; remove this TODO when that Server capability exists. + throw e; + } finally { + timer.stop(); + long duration = timer.getTime(TimeUnit.MILLISECONDS); + history.setStatus(status); + history.setDuration(duration); + history.setAsyncId(asyncId); + this.historyService.update(history); + } + } + + private void checkParamsValid(GremlinQuery query) { + Ex.check(!org.apache.commons.lang3.StringUtils.isEmpty(query.getContent()), + "common.param.cannot-be-null-or-empty", + "gremlin-query.content"); + GremlinController.checkContentLength(query.getContent()); + } + + public void initBuiltIn(HugeClient client, GraphConnection connection, + BuiltInEntity entity) { + List graphs = client.graphs().listGraph(); + if (entity.initHlm) { + initHlm(client, graphs.contains(GRAPH_HLM)); + } + + client.assignGraph(Constant.BUILT_IN, null); + if (entity.initCovid19) { + connection.setGraph(GRAPH_COVID19); + initCovid19(client, graphs.contains(GRAPH_COVID19), connection); + } + } + + public void initHlm(HugeClient client, boolean exist) { + if (!exist) { + this.create(client, "红楼梦", GRAPH_HLM, null); + } else { + this.update(client, "红楼梦", GRAPH_HLM); + this.clearGraph(client, GRAPH_HLM); + } + + GremlinQuery query = new GremlinQuery(GREMLIN_LOAD_HLM); + client.assignGraph(Constant.BUILT_IN, GRAPH_HLM); + this.queryService.executeGremlinQuery(client, query); + } + + public void initCovid19(HugeClient client, boolean exist, + GraphConnection connection) { + if (!exist) { + this.create(client, "新冠患者轨迹追溯", GRAPH_COVID19, null); + } else { + this.update(client, "新冠患者轨迹追溯", GRAPH_COVID19); + this.clearGraph(client, GRAPH_COVID19); + } + + // todo load data + loadTaskService.startCovid19(connection, Constant.BUILT_IN, + GRAPH_COVID19, client); + } + + /** + * 统计指定单个图中的顶点总数和边总数 + */ + public Map evCount(HugeClient client, + String graphSpace, + String graph) { + Map res = new HashMap<>(); + long edgeCount = 0L; + long vertexCount = 0L; + String statisticDate = HubbleUtil.dateFormatDay(HubbleUtil.nowDate()); + client.assignGraph(graphSpace, graph); + GraphMetricsAPI.ElementCount statistic = + client.graph().getEVCount(statisticDate); + if (statistic == null) { + statisticDate = HubbleUtil.dateFormatLastDay(); + statistic = client.graph().getEVCount(statisticDate); + } + + if (statistic != null) { + vertexCount = statistic.getVertices(); + edgeCount += statistic.getEdges(); + } + + res.put("date", statisticDate); + res.put("vertex", vertexCount); + res.put("edge", edgeCount); + return res; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/license/LicenseService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/license/LicenseService.java index 72bb57153..c3ba79a0e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/license/LicenseService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/license/LicenseService.java @@ -16,177 +16,180 @@ * under the License. */ -package org.apache.hugegraph.service.license; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.driver.HugeClient; -import org.apache.hugegraph.entity.GraphConnection; -import org.apache.hugegraph.handler.MessageSourceHandler; -import org.apache.hugegraph.service.GraphConnectionService; -import org.apache.hugegraph.service.HugeClientPoolService; -import org.apache.hugegraph.util.Ex; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Service -public class LicenseService { - - private static final String METRICS_DATA_SIZE = "data_size"; - - @Autowired - private GraphConnectionService connService; - @Autowired - private HugeClientPoolService poolService; - @Autowired - private MessageSourceHandler messageHandler; - - @Data - @AllArgsConstructor - public class VerifyResult { - - private final boolean enabled; - private final String graphsMessage; - private final List dataSizeMessages; - - public VerifyResult(boolean enabled) { - this(enabled, null); - } - - public VerifyResult(boolean enabled, String graphsMessage) { - this.enabled = enabled; - this.graphsMessage = graphsMessage; - this.dataSizeMessages = new ArrayList<>(); - } - - public void add(String disableReason) { - this.dataSizeMessages.add(disableReason); - } - - public String getMessage() { - if (this.enabled) { - return null; - } - - String comma = LicenseService.this.getMessage("common.joiner.comma"); - String semicolon = LicenseService.this.getMessage( - "common.joiner.semicolon"); - - StringBuilder sb = new StringBuilder(); - sb.append(LicenseService.this.getMessage( - "license.verify.graph-connection.failed.preifx")); - sb.append(comma); - if (!StringUtils.isEmpty(this.graphsMessage)) { - sb.append(this.graphsMessage); - sb.append(semicolon); - } - if (!this.dataSizeMessages.isEmpty()) { - for (String dataSizeMsg : this.dataSizeMessages) { - if (!StringUtils.isEmpty(dataSizeMsg)) { - sb.append(dataSizeMsg); - sb.append(comma); - } - } - } - sb.deleteCharAt(sb.length() - 1); - sb.append(comma); - sb.append(LicenseService.this.getMessage( - "license.verify.graph-connection.failed.suffix")); - return sb.toString(); - } - } - - public VerifyResult verifyGraphs(int actualGraphs) { - return new VerifyResult(true); - } - - @Async - @Scheduled(fixedRate = 3 * 60 * 1000) - public void updateAllGraphStatus() { - List connections = this.connService.listAll(); - for (GraphConnection conn : connections) { - this.updateGraphStatus(conn); - } - } - - private void updateGraphStatus(GraphConnection conn) { - HugeClient client; - try { - client = this.poolService.getOrCreate(conn.getId()); - } catch (Exception e) { - String msg = this.getMessage("graph-connection.client.unavailable", - conn.getName()); - conn.setEnabled(false); - conn.setDisableReason(msg); - this.connService.update(conn); - return; - } - - conn.setEnabled(true); - conn.setDisableReason(""); - this.connService.update(conn); - } - - private String getMessage(String msgKey, Object... args) { - return this.messageHandler.getMessage(msgKey, args); - } - - /** - * Keep 2 method for future use now - */ - private static long getActualDataSize(HugeClient client, String graph) { - Map metrics = client.metrics().backend(graph); - Object dataSize = metrics.get(METRICS_DATA_SIZE); - if (dataSize == null) { - return 0L; - } - Ex.check(dataSize instanceof String, - "The backend metrics data_size must be String type, " + - "but got '%s'(%s)", dataSize, dataSize.getClass()); - // Unit is MB - return displaySizeToMB((String) dataSize); - } - - private static long displaySizeToMB(String displaySize) { - String[] parts = displaySize.split(" "); - Ex.check(parts.length == 2, - "The displaySize must be formatted as two parts"); - long numberPart = Long.parseLong(parts[0]); - long byteCount = 0L; - switch (parts[1]) { - case "bytes": - byteCount = numberPart; - break; - case "KB": - byteCount = numberPart * FileUtils.ONE_KB; - break; - case "MB": - byteCount = numberPart * FileUtils.ONE_MB; - break; - case "GB": - byteCount = numberPart * FileUtils.ONE_GB; - break; - case "TB": - byteCount = numberPart * FileUtils.ONE_TB; - break; - case "PB": - byteCount = numberPart * FileUtils.ONE_PB; - break; - case "EB": - byteCount = numberPart * FileUtils.ONE_EB; - break; - default: - break; - } - return byteCount / FileUtils.ONE_MB; - } -} +package org.apache.hugegraph.service.license;///* +// * Copyright 2017 HugeGraph Authors +// * +// * Licensed to the Apache Software Foundation (ASF) under one or more +// * contributor license agreements. See the NOTICE file distributed with this +// * work for additional information regarding copyright ownership. The ASF +// * licenses this file to You 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. +// */ +// // TODO C Remove Licence +//package org.apache.hugegraph.service.license; +// +//import java.util.ArrayList; +//import java.util.List; +//import java.util.Map; +// +//import org.apache.commons.io.FileUtils; +//import org.apache.commons.lang3.StringUtils; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.stereotype.Service; +// +//import org.apache.hugegraph.common.Constant; +//import org.apache.hugegraph.driver.HugeClient; +//import org.apache.hugegraph.handler.MessageSourceHandler; +////import org.apache.hugegraph.license.LicenseVerifier; // TODO C Remove Licence +//import org.apache.hugegraph.util.Ex; +// +//import lombok.AllArgsConstructor; +//import lombok.Data; +// +//@Service +//public class LicenseService { +// +// private static final String METRICS_DATA_SIZE = "data_size"; +// +// @Autowired +// private MessageSourceHandler messageHandler; +// +// @Data +// @AllArgsConstructor +// public class VerifyResult { +// +// private boolean enabled; +// private String graphsMessage; +// private List dataSizeMessages; +// +// public VerifyResult(boolean enabled) { +// this(enabled, null); +// } +// +// public VerifyResult(boolean enabled, String graphsMessage) { +// this.enabled = enabled; +// this.graphsMessage = graphsMessage; +// this.dataSizeMessages = new ArrayList<>(); +// } +// +// public void add(String disableReason) { +// this.dataSizeMessages.add(disableReason); +// } +// +// public String getMessage() { +// if (this.enabled) { +// return null; +// } +// +// String comma = LicenseService.this.getMessage("common.joiner.comma"); +// String semicolon = LicenseService.this.getMessage( +// "common.joiner.semicolon"); +// +// StringBuilder sb = new StringBuilder(); +// sb.append(LicenseService.this.getMessage( +// "license.verify.graph-connection.failed.preifx")); +// sb.append(comma); +// if (!StringUtils.isEmpty(this.graphsMessage)) { +// sb.append(this.graphsMessage); +// sb.append(semicolon); +// } +// if (!this.dataSizeMessages.isEmpty()) { +// for (String dataSizeMsg : this.dataSizeMessages) { +// if (!StringUtils.isEmpty(dataSizeMsg)) { +// sb.append(dataSizeMsg); +// sb.append(comma); +// } +// } +// } +// sb.deleteCharAt(sb.length() - 1); +// sb.append(comma); +// sb.append(LicenseService.this.getMessage( +// "license.verify.graph-connection.failed.suffix")); +// return sb.toString(); +// } +// } +// +// public VerifyResult verifyGraphs(int actualGraphs) { +// int allowedGraphs = LicenseVerifier.instance().allowedGraphs(); +// if (allowedGraphs != Constant.NO_LIMIT && +// actualGraphs > allowedGraphs) { +// String msg = this.getMessage("license.verify.graphs.exceed", +// actualGraphs, allowedGraphs); +// return new VerifyResult(false, msg); +// } else { +// return new VerifyResult(true); +// } +// } +// +// public VerifyResult verifyDataSize(HugeClient client, String name, +// String graph) { +// long allowedDataSize = LicenseVerifier.instance().allowedDataSize(); +// long actualDataSize = getActualDataSize(client, graph); +// if (allowedDataSize != Constant.NO_LIMIT && +// actualDataSize > allowedDataSize) { +// String msg = this.getMessage("license.verify.datasize.exceed", +// name, actualDataSize, allowedDataSize); +// return new VerifyResult(false, msg); +// } else { +// return new VerifyResult(true); +// } +// } +// +// private String getMessage(String msgKey, Object... args) { +// return this.messageHandler.getMessage(msgKey, args); +// } +// +// private static long getActualDataSize(HugeClient client, String graph) { +// Map metrics = client.metrics().backend(graph); +// Object dataSize = metrics.get(METRICS_DATA_SIZE); +// if (dataSize == null) { +// return 0L; +// } +// Ex.check(dataSize instanceof String, +// "The backend metrics data_size must be String type, " + +// "but got '%s'(%s)", dataSize, dataSize.getClass()); +// // Unit is MB +// return displaySizeToMB((String) dataSize); +// } +// +// private static long displaySizeToMB(String displaySize) { +// String[] parts = displaySize.split(" "); +// Ex.check(parts.length == 2, +// "The displaySize must be formatted as two parts"); +// long numberPart = Long.parseLong(parts[0]); +// long byteCount = 0L; +// switch (parts[1]) { +// case "bytes": +// byteCount = numberPart; +// break; +// case "KB": +// byteCount = numberPart * FileUtils.ONE_KB; +// break; +// case "MB": +// byteCount = numberPart * FileUtils.ONE_MB; +// break; +// case "GB": +// byteCount = numberPart * FileUtils.ONE_GB; +// break; +// case "TB": +// byteCount = numberPart * FileUtils.ONE_TB; +// break; +// case "PB": +// byteCount = numberPart * FileUtils.ONE_PB; +// break; +// case "EB": +// byteCount = numberPart * FileUtils.ONE_EB; +// break; +// } +// return byteCount / FileUtils.ONE_MB; +// } +//} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/DatasourceService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/DatasourceService.java new file mode 100644 index 000000000..62dc3a4f1 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/DatasourceService.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.load; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.hugegraph.entity.load.Datasource; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.mapper.load.DatasourceMapper; +import org.apache.hugegraph.util.HubbleUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DatasourceService { + + @Autowired + private DatasourceMapper mapper; + + public Datasource get(int id) { + return this.mapper.selectById(id); + } + + public IPage list(int pageNo, int pageSize, String query) { + QueryWrapper wrapper = Wrappers.query(); + if (query != null && !query.isEmpty()) { + wrapper.like("datasource_name", query); + } + wrapper.orderByDesc("create_time"); + return this.mapper.selectPage(new Page<>(pageNo, pageSize), wrapper); + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public void save(Datasource entity) { + entity.setCreateTime(HubbleUtil.nowDate()); + if (this.mapper.insert(entity) != 1) { + throw new InternalException("entity.insert.failed", entity); + } + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public void remove(int id) { + if (this.mapper.deleteById(id) != 1) { + throw new InternalException("entity.delete.failed", id); + } + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public void removeBatch(java.util.List ids) { + this.mapper.deleteBatchIds(ids); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/FileMappingService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/FileMappingService.java index f174c890c..06495a763 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/FileMappingService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/FileMappingService.java @@ -46,6 +46,14 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.entity.enums.FileMappingStatus; import org.apache.hugegraph.entity.load.FileMapping; @@ -59,14 +67,6 @@ import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.HubbleUtil; import org.apache.hugegraph.util.StringUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -103,9 +103,20 @@ public FileMapping get(int id) { return this.mapper.selectById(id); } - public FileMapping get(int connId, int jobId, String fileName) { + public FileMapping get(String graphSpace, String graph, int jobId, int id) { QueryWrapper query = Wrappers.query(); - query.eq("conn_id", connId) + query.eq("id", id) + .eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("job_id", jobId); + return this.mapper.selectOne(query); + } + + public FileMapping get(String graphSpace, String graph, int jobId, + String fileName) { + QueryWrapper query = Wrappers.query(); + query.eq("graphspace", graphSpace) + .eq("graph", graph) .eq("job_id", jobId) .eq("name", fileName); return this.mapper.selectOne(query); @@ -121,9 +132,20 @@ public List listByJob(int jobId) { return this.mapper.selectList(query); } - public IPage list(int connId, int jobId, int pageNo, int pageSize) { + public List listByJob(String graphSpace, String graph, + int jobId) { + QueryWrapper query = Wrappers.query(); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("job_id", jobId); + return this.mapper.selectList(query); + } + + public IPage list(String graphSpace, String graph, int jobId, + int pageNo, int pageSize) { QueryWrapper query = Wrappers.query(); - query.eq("conn_id", connId); + query.eq("graphspace", graphSpace); + query.eq("graph", graph); query.eq("job_id", jobId); query.eq("file_status", FileMappingStatus.COMPLETED.getValue()); query.orderByDesc("create_time"); @@ -157,11 +179,11 @@ public String generateFileToken(String fileName) { HubbleUtil.nowTime().getEpochSecond(); } - public FileUploadResult uploadFile(MultipartFile srcFile, int index, - String dirPath) { + public FileUploadResult uploadFile(MultipartFile srcFile, String fileName, + int index, String dirPath) { FileUploadResult result = new FileUploadResult(); // Current part saved path - String partName = srcFile.getOriginalFilename(); + String partName = fileName; result.setName(partName); result.setSize(srcFile.getSize()); @@ -202,7 +224,18 @@ public boolean tryMergePartFiles(String dirPath, int total) { File newFile = new File(dir.getPath() + ".all"); File destFile = new File(dir.getPath()); + if (newFile.exists()) { + try { + FileUtils.forceDelete(newFile); + } catch (IOException e) { + log.error("Failed to delete stale merged upload file {}", + newFile, e); + throw new InternalException("load.upload.delete-temp-dir.failed", + e); + } + } if (partFiles.length == 1) { + this.checkPartFileIndex(partFiles[0], 0); try { // Rename file to dest file FileUtils.moveFile(partFiles[0], newFile); @@ -221,16 +254,17 @@ public boolean tryMergePartFiles(String dirPath, int total) { Integer idx2 = Integer.valueOf(file2Idx); return idx1.compareTo(idx2); }); - try (OutputStream os = new FileOutputStream(newFile, true)) { + try (OutputStream os = new FileOutputStream(newFile, false)) { for (int i = 0; i < partFiles.length; i++) { File partFile = partFiles[i]; + this.checkPartFileIndex(partFile, i); try (InputStream is = new FileInputStream(partFile)) { IOUtils.copy(is, os); } catch (IOException e) { log.error("Failed to copy file stream from {} to {}", partFile, newFile, e); throw new InternalException( - "load.upload.merge-file.failed", e); + "load.upload.merge-file.failed", e); } } } catch (IOException e) { @@ -253,6 +287,20 @@ public boolean tryMergePartFiles(String dirPath, int total) { return true; } + private void checkPartFileIndex(File partFile, int expectedIndex) { + String fileIdx = StringUtils.substringAfterLast(partFile.getName(), + "-"); + try { + int actualIndex = Integer.parseInt(fileIdx); + if (actualIndex == expectedIndex) { + return; + } + } catch (NumberFormatException ignored) { + // Fall through to a uniform error below. + } + throw new InternalException("load.upload.merge-file.failed"); + } + public void extractColumns(FileMapping mapping) { File file = this.requirePathUnderUploadRoot(mapping.getPath()); BufferedReader reader; @@ -297,7 +345,11 @@ public void extractColumns(FileMapping mapping) { throw new InternalException("Failed to read header and sample " + "data from file '%s'", file); } finally { - IOUtils.closeQuietly(reader); + try { + reader.close(); + } catch (IOException ignored) { + log.debug("Failed to close file mapping reader", ignored); + } } setting.setColumnNames(Arrays.asList(columnNames)); @@ -315,7 +367,7 @@ public String moveToNextLevelDir(FileMapping mapping) { } catch (IOException e) { this.remove(mapping.getId()); throw new InternalException( - "Failed to move file to next level directory"); + "Failed to move file to next level directory"); } return Paths.get(destPath, currFile.getName()).toString(); } @@ -354,7 +406,7 @@ public void deleteUnfinishedFile() { query.in("file_status", FileMappingStatus.UPLOADING.getValue()); List mappings = this.mapper.selectList(query); long threshold = this.config.get( - HubbleOptions.UPLOAD_FILE_MAX_TIME_CONSUMING) * 1000; + HubbleOptions.UPLOAD_FILE_MAX_TIME_CONSUMING) * 1000; Date now = HubbleUtil.nowDate(); for (FileMapping mapping : mappings) { Date updateTime = mapping.getUpdateTime(); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java index eb085c390..9d19a9523 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java @@ -22,6 +22,13 @@ import java.util.Date; import java.util.List; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.ImmutableMap; +import lombok.extern.log4j.Log4j2; + import org.apache.hugegraph.entity.enums.JobStatus; import org.apache.hugegraph.entity.enums.LoadStatus; import org.apache.hugegraph.entity.load.FileMapping; @@ -38,13 +45,6 @@ import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; - -import lombok.extern.log4j.Log4j2; - @Log4j2 @Service public class JobManagerService { @@ -64,20 +64,36 @@ public JobManager get(int id) { return this.mapper.selectById(id); } - public JobManager getTask(String jobName, int connId) { + public JobManager get(String graphSpace, String graph, int id) { + QueryWrapper query = Wrappers.query(); + query.eq("id", id) + .eq("graphspace", graphSpace) + .eq("graph", graph); + return this.mapper.selectOne(query); + } + + public JobManager getTask(String jobName, String graphSpace, String graph) { QueryWrapper query = Wrappers.query(); query.eq("job_name", jobName); - query.eq("conn_id", connId); + query.eq("graphspace", graphSpace); + query.eq("graph", graph); return this.mapper.selectOne(query); } - public List list(int connId, List jobIds) { - return this.mapper.selectBatchIds(jobIds); + public List list(String graphSpace, String graph, + List jobIds) { + QueryWrapper query = Wrappers.query(); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .in("id", jobIds); + return this.mapper.selectList(query); } - public IPage list(int connId, int pageNo, int pageSize, String content) { + public IPage list(String graphSpace, String graph, + int pageNo, int pageSize, String content) { QueryWrapper query = Wrappers.query(); - query.eq("conn_id", connId); + query.eq("graphspace", graphSpace); + query.eq("graph", graph); if (!content.isEmpty()) { query.like("job_name", content); } @@ -85,28 +101,7 @@ public IPage list(int connId, int pageNo, int pageSize, String conte Page page = new Page<>(pageNo, pageSize); IPage list = this.mapper.selectPage(page, query); list.getRecords().forEach(task -> { - if (task.getJobStatus() == JobStatus.LOADING) { - List tasks = this.taskService.taskListByJob(task.getId()); - JobStatus status = JobStatus.SUCCESS; - for (LoadTask loadTask : tasks) { - if (loadTask.getStatus().inRunning() || - loadTask.getStatus() == LoadStatus.PAUSED || - loadTask.getStatus() == LoadStatus.STOPPED) { - status = JobStatus.LOADING; - break; - } - if (loadTask.getStatus() == LoadStatus.FAILED) { - status = JobStatus.FAILED; - break; - } - } - - if (status == JobStatus.SUCCESS || - status == JobStatus.FAILED) { - task.setJobStatus(status); - this.update(task); - } - } + this.refreshStatus(task); Date endDate = task.getJobStatus() == JobStatus.FAILED || task.getJobStatus() == JobStatus.SUCCESS ? task.getUpdateTime() : HubbleUtil.nowDate(); @@ -119,6 +114,46 @@ public List listAll() { return this.mapper.selectList(null); } + public IPage listAll(int pageNo, int pageSize, String content) { + QueryWrapper query = Wrappers.query(); + if (content != null && !content.isEmpty()) { + query.like("job_name", content); + } + query.orderByDesc("create_time"); + IPage list = this.mapper.selectPage(new Page<>(pageNo, + pageSize), + query); + list.getRecords().forEach(this::refreshStatus); + return list; + } + + public void refreshStatus(JobManager task) { + if (task == null || task.getJobStatus() != JobStatus.LOADING) { + return; + } + + List tasks = this.taskService.taskListByJob(task.getId()); + JobStatus status = JobStatus.SUCCESS; + for (LoadTask loadTask : tasks) { + if (loadTask.getStatus().inRunning() || + loadTask.getStatus() == LoadStatus.PAUSED || + loadTask.getStatus() == LoadStatus.STOPPED) { + status = JobStatus.LOADING; + break; + } + if (loadTask.getStatus() == LoadStatus.FAILED) { + status = JobStatus.FAILED; + break; + } + } + + if (status == JobStatus.SUCCESS || status == JobStatus.FAILED) { + task.setJobStatus(status); + task.setUpdateTime(HubbleUtil.nowDate()); + this.update(task); + } + } + @Transactional(isolation = Isolation.READ_COMMITTED) public void save(JobManager entity) { if (this.mapper.insert(entity) != 1) { @@ -140,6 +175,12 @@ public void remove(int id) { } } + @Transactional(isolation = Isolation.READ_COMMITTED) + public void removeByGraph(String graphSpace, String graph) { + this.mapper.deleteByMap(ImmutableMap.of("graphspace", graphSpace, + "graph", graph)); + } + @Transactional(isolation = Isolation.READ_COMMITTED) public void deleteJob(int id) { JobManager job = this.get(id); @@ -161,6 +202,15 @@ public void deleteJob(int id) { this.deleteDiskFilesAfterCommit(mappings); } + @Transactional(isolation = Isolation.READ_COMMITTED) + public void deleteJob(String graphSpace, String graph, int id) { + JobManager job = this.get(graphSpace, graph, id); + if (job == null) { + throw new ExternalException("job.manager.not-exist.id", id); + } + this.deleteJob(id); + } + private void deleteDiskFilesAfterCommit(List mappings) { if (mappings.isEmpty()) { return; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java index a5d9dc515..76f3af7ca 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java @@ -18,19 +18,16 @@ package org.apache.hugegraph.service.load; -import java.io.File; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.ImmutableList; +import lombok.extern.log4j.Log4j2; import org.apache.commons.io.FileUtils; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.GraphConnection; import org.apache.hugegraph.entity.enums.LoadStatus; import org.apache.hugegraph.entity.load.EdgeMapping; @@ -45,6 +42,7 @@ import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.exception.InternalException; import org.apache.hugegraph.handler.LoadTaskExecutor; +import org.apache.hugegraph.loader.HugeGraphLoader; import org.apache.hugegraph.loader.executor.LoadContext; import org.apache.hugegraph.loader.executor.LoadOptions; import org.apache.hugegraph.loader.mapping.InputStruct; @@ -52,8 +50,8 @@ import org.apache.hugegraph.loader.source.file.FileFormat; import org.apache.hugegraph.loader.source.file.FileSource; import org.apache.hugegraph.loader.util.MappingUtil; +import org.apache.hugegraph.loader.util.Printer; import org.apache.hugegraph.mapper.load.LoadTaskMapper; -import org.apache.hugegraph.service.SettingSSLService; import org.apache.hugegraph.service.schema.EdgeLabelService; import org.apache.hugegraph.service.schema.VertexLabelService; import org.apache.hugegraph.util.Ex; @@ -64,13 +62,18 @@ import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.google.common.collect.ImmutableList; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; -import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; @Log4j2 @Service @@ -85,10 +88,9 @@ public class LoadTaskService { @Autowired private LoadTaskExecutor taskExecutor; @Autowired - private SettingSSLService sslService; - @Autowired private HugeConfig config; + private Map runningTaskContainer; public LoadTaskService() { @@ -99,21 +101,38 @@ public LoadTask get(int id) { return this.mapper.selectById(id); } + public LoadTask get(String graphSpace, String graph, int jobId, int id) { + QueryWrapper query = Wrappers.query(); + query.eq("id", id) + .eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("job_id", jobId); + return this.mapper.selectOne(query); + } + public List listAll() { return this.mapper.selectList(null); } - public IPage list(int connId, int jobId, int pageNo, int pageSize) { + public IPage list(String graphSpace, String graph, int jobId, + int pageNo, int pageSize) { QueryWrapper query = Wrappers.query(); - query.eq("conn_id", connId); + query.eq("graphspace", graphSpace); + query.eq("graph", graph); query.eq("job_id", jobId); query.orderByDesc("create_time"); Page page = new Page<>(pageNo, pageSize); return this.mapper.selectPage(page, query); } - public List list(int connId, List taskIds) { - return this.mapper.selectBatchIds(taskIds); + public List list(String graphSpace, String graph, int jobId, + List taskIds) { + QueryWrapper query = Wrappers.query(); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("job_id", jobId) + .in("id", taskIds); + return this.mapper.selectList(query); } public int count() { @@ -160,9 +179,9 @@ public List batchTasks(int jobId) { return this.mapper.selectList(query); } - public LoadTask start(GraphConnection connection, FileMapping fileMapping) { - this.sslService.configSSL(this.config, connection); - LoadTask task = this.buildLoadTask(connection, fileMapping); + public LoadTask start(GraphConnection connection, FileMapping fileMapping, + HugeClient client) { + LoadTask task = this.buildLoadTask(connection, fileMapping, client); this.save(task); // Executed in other threads this.taskExecutor.execute(task, () -> this.update(task)); @@ -272,7 +291,8 @@ public String readLoadFailedReason(FileMapping mapping) { errorFiles.length); File errorFile = errorFiles[0]; try { - return FileUtils.readFileToString(errorFile); + return FileUtils.readFileToString(errorFile, + Charset.defaultCharset()); } catch (IOException e) { throw new InternalException("Failed to read error file %s", e, errorFile); @@ -305,13 +325,6 @@ public void updateLoadTaskProgress() { LoadContext context = task.context(); long readLines = context.newProgress().totalInputRead(); if (readLines == 0L) { - /* - * When the Context is just constructed, newProgress - * is empty. Only after parsing is started will use - * oldProgress and incrementally update newProgress, - * if get totalInputRead value during this process, - * it will return 0, so need read it from oldProgress - */ readLines = context.oldProgress().totalInputRead(); } task.setFileReadLines(readLines); @@ -325,11 +338,12 @@ public void updateLoadTaskProgress() { } private LoadTask buildLoadTask(GraphConnection connection, - FileMapping fileMapping) { + FileMapping fileMapping, HugeClient client) { try { LoadOptions options = this.buildLoadOptions(connection, fileMapping); // NOTE: For simplicity, one file corresponds to one import task - LoadMapping mapping = this.buildLoadMapping(connection, fileMapping); + LoadMapping mapping = this.buildLoadMapping(connection, fileMapping, + client); this.bindMappingToOptions(options, mapping, fileMapping.getPath()); return new LoadTask(options, connection, fileMapping); } catch (Exception e) { @@ -353,18 +367,40 @@ private void bindMappingToOptions(LoadOptions options, LoadMapping mapping, private LoadOptions buildLoadOptions(GraphConnection connection, FileMapping fileMapping) { LoadOptions options = new LoadOptions(); - // Fill with input and server params + // Connection params options.file = fileMapping.getPath(); - // No need to specify a schema file - options.host = connection.getHost(); - options.port = connection.getPort(); - options.graph = connection.getGraph(); + boolean directServer = StringUtils.isNotEmpty(connection.getHost()) || + connection.getPort() != null; + if (StringUtils.isNotEmpty(connection.getGraphSpace())) { + options.graphSpace = connection.getGraphSpace(); + } + if (StringUtils.isNotEmpty(connection.getGraph())) { + options.graph = connection.getGraph(); + } + if (!directServer && StringUtils.isNotEmpty(connection.getPdPeers())) { + options.pdPeers = connection.getPdPeers(); + } + if (StringUtils.isNotEmpty(connection.getCluster())) { + options.cluster = connection.getCluster(); + } + if (StringUtils.isNotEmpty(connection.getRouteType())) { + options.routeType = connection.getRouteType(); + } + if (StringUtils.isNotEmpty(connection.getHost())) { + options.host = connection.getHost(); + } + if (connection.getPort() != null) { + options.port = connection.getPort(); + } options.username = connection.getUsername(); - options.token = connection.getPassword(); - options.protocol = connection.getProtocol(); - options.trustStoreFile = connection.getTrustStoreFile(); - options.trustStoreToken = connection.getTrustStorePassword(); - // Fill with load parameters + if (StringUtils.isNotEmpty(connection.getPassword())) { + options.password = connection.getPassword(); + } else { + options.token = connection.getToken(); + } + options.protocol = StringUtils.isNotEmpty(connection.getProtocol()) ? + connection.getProtocol() : "http"; + // Load parameters LoadParameter parameter = fileMapping.getLoadParameter(); options.checkVertex = parameter.isCheckVertex(); options.timeout = parameter.getInsertTimeout(); @@ -373,7 +409,7 @@ private LoadOptions buildLoadOptions(GraphConnection connection, options.maxInsertErrors = parameter.getMaxInsertErrors(); options.retryTimes = parameter.getRetryTimes(); options.retryInterval = parameter.getRetryInterval(); - // Optimized for hubble + // Optimized for hubble (conservative defaults) options.batchInsertThreads = 4; options.singleInsertThreads = 4; options.batchSize = 100; @@ -381,17 +417,24 @@ private LoadOptions buildLoadOptions(GraphConnection connection, } private LoadMapping buildLoadMapping(GraphConnection connection, - FileMapping fileMapping) { + FileMapping fileMapping, + HugeClient client) { FileSource source = this.buildFileSource(fileMapping); + log.info("Building load mapping for file: {}, vertices: {}, edges: {}", + fileMapping.getName(), + fileMapping.getVertexMappings().size(), + fileMapping.getEdgeMappings().size()); List vMappings; - vMappings = this.buildVertexMappings(connection, fileMapping); + vMappings = this.buildVertexMappings(connection, fileMapping, client); List eMappings; - eMappings = this.buildEdgeMappings(connection, fileMapping); + eMappings = this.buildEdgeMappings(connection, fileMapping, client); InputStruct inputStruct = new InputStruct(vMappings, eMappings); inputStruct.id("1"); inputStruct.input(source); + log.info("Built InputStruct id={}, vertices={}, edges={}", + inputStruct.id(), inputStruct.vertices().size(), inputStruct.edges().size()); return new LoadMapping(ImmutableList.of(inputStruct)); } @@ -421,36 +464,40 @@ private FileSource buildFileSource(FileMapping fileMapping) { } private List - buildVertexMappings(GraphConnection connection, - FileMapping fileMapping) { - int connId = connection.getId(); + buildVertexMappings(GraphConnection connection, + FileMapping fileMapping, HugeClient client) { List vMappings = new ArrayList<>(); for (VertexMapping mapping : fileMapping.getVertexMappings()) { - VertexLabelEntity vl = this.vlService.get(mapping.getLabel(), connId); + VertexLabelEntity vl = this.vlService.get(mapping.getLabel(), + client); List idFields = mapping.getIdFields(); Map fieldMappings = mapping.fieldMappingToMap(); + org.apache.hugegraph.loader.mapping.VertexMapping vMapping; if (vl.getIdStrategy().isCustomize()) { Ex.check(idFields.size() == 1, "When the ID strategy is CUSTOMIZED, you must " + "select a column in the file as the id"); - vMapping = new org.apache.hugegraph.loader.mapping.VertexMapping(idFields.get(0), - true); + vMapping = new org.apache.hugegraph.loader.mapping.VertexMapping( + idFields.get(0), true); } else { - assert vl.getIdStrategy().isPrimaryKey(); + Ex.check(vl.getIdStrategy().isPrimaryKey(), + "Unsupported vertex id strategy: %s", + vl.getIdStrategy()); List primaryKeys = vl.getPrimaryKeys(); + if (idFields == null || idFields.isEmpty()) { + idFields = this.inferPrimaryKeyFields(fieldMappings, + primaryKeys); + } Ex.check(idFields.size() >= 1 && idFields.size() == primaryKeys.size(), "When the ID strategy is PRIMARY_KEY, you must " + "select at least one column in the file as the " + "primary keys"); - /* - * The id column can be unfold into multi sub-ids only - * when primarykeys contains just one field - */ boolean unfold = idFields.size() == 1; - vMapping = new org.apache.hugegraph.loader.mapping.VertexMapping(null, unfold); + vMapping = new org.apache.hugegraph.loader.mapping.VertexMapping( + null, unfold); for (int i = 0; i < primaryKeys.size(); i++) { fieldMappings.put(idFields.get(i), primaryKeys.get(i)); } @@ -461,7 +508,7 @@ private FileSource buildFileSource(FileMapping fileMapping) { vMapping.mappingFields(fieldMappings); // set value_mapping vMapping.mappingValues(mapping.valueMappingToMap()); - // set selected + // set selected fields vMapping.selectedFields().addAll(idFields); vMapping.selectedFields().addAll(fieldMappings.keySet()); // set null_values @@ -469,37 +516,45 @@ private FileSource buildFileSource(FileMapping fileMapping) { nullValues.addAll(mapping.getNullValues().getChecked()); nullValues.addAll(mapping.getNullValues().getCustomized()); vMapping.nullValues(nullValues); - // TODO: Update strategies + vMappings.add(vMapping); } return vMappings; } + private List inferPrimaryKeyFields(Map fieldMappings, + List primaryKeys) { + List idFields = new ArrayList<>(); + for (String primaryKey : primaryKeys) { + for (Map.Entry entry : fieldMappings.entrySet()) { + if (primaryKey.equals(entry.getValue())) { + idFields.add(entry.getKey()); + break; + } + } + } + return idFields; + } + private List - buildEdgeMappings(GraphConnection connection, - FileMapping fileMapping) { - int connId = connection.getId(); + buildEdgeMappings(GraphConnection connection, + FileMapping fileMapping, HugeClient client) { List eMappings = new ArrayList<>(); for (EdgeMapping mapping : fileMapping.getEdgeMappings()) { List sourceFields = mapping.getSourceFields(); List targetFields = mapping.getTargetFields(); - EdgeLabelEntity el = this.elService.get(mapping.getLabel(), connId); - VertexLabelEntity svl = this.vlService.get(el.getSourceLabel(), - connId); - VertexLabelEntity tvl = this.vlService.get(el.getTargetLabel(), - connId); + EdgeLabelEntity el = this.elService.get(mapping.getLabel(), client); + VertexLabelEntity svl = this.vlService.get(el.getSourceLabel(), client); + VertexLabelEntity tvl = this.vlService.get(el.getTargetLabel(), client); Map fieldMappings = mapping.fieldMappingToMap(); - /* - * When id strategy is customize or primaryKeys contains - * just one field, the param 'unfold' can be true - */ + boolean unfoldSource = true; if (svl.getIdStrategy().isPrimaryKey()) { List primaryKeys = svl.getPrimaryKeys(); Ex.check(sourceFields.size() >= 1 && sourceFields.size() == primaryKeys.size(), - "When the source vertex ID strategy is CUSTOMIZED, " + + "When the source vertex ID strategy is PRIMARY_KEY, " + "you must select at least one column in the file " + "as the id"); for (int i = 0; i < primaryKeys.size(); i++) { @@ -514,7 +569,7 @@ private FileSource buildFileSource(FileMapping fileMapping) { List primaryKeys = tvl.getPrimaryKeys(); Ex.check(targetFields.size() >= 1 && targetFields.size() == primaryKeys.size(), - "When the target vertex ID strategy is CUSTOMIZED, " + + "When the target vertex ID strategy is PRIMARY_KEY, " + "you must select at least one column in the file " + "as the id"); for (int i = 0; i < primaryKeys.size(); i++) { @@ -525,16 +580,16 @@ private FileSource buildFileSource(FileMapping fileMapping) { } } - org.apache.hugegraph.loader.mapping.EdgeMapping eMapping; - eMapping = new org.apache.hugegraph.loader.mapping.EdgeMapping( - sourceFields, unfoldSource, targetFields, unfoldTarget); + org.apache.hugegraph.loader.mapping.EdgeMapping eMapping = + new org.apache.hugegraph.loader.mapping.EdgeMapping( + sourceFields, unfoldSource, targetFields, unfoldTarget); // set label eMapping.label(mapping.getLabel()); // set field_mapping eMapping.mappingFields(fieldMappings); // set value_mapping eMapping.mappingValues(mapping.valueMappingToMap()); - // set selected + // set selected fields eMapping.selectedFields().addAll(sourceFields); eMapping.selectedFields().addAll(targetFields); eMapping.selectedFields().addAll(fieldMappings.keySet()); @@ -548,4 +603,34 @@ private FileSource buildFileSource(FileMapping fileMapping) { } return eMappings; } + + public void startCovid19(GraphConnection connection, + String graphSpace, String graph, + HugeClient client) { + FileMapping fileMapping = + new FileMapping(graphSpace, graph, "covid19", + "example/covid19/struct.json"); + LoadParameter loadParameter = new LoadParameter(); + fileMapping.setLoadParameter(loadParameter); + + LoadOptions options = this.buildLoadOptions(connection, fileMapping); + // options.direct = true; + // options.pdPeers = connection.getPdPeers(); + options.schema = "example/covid19/schema.groovy"; + options.host = connection.getHost(); + options.port = connection.getPort(); + options.protocol = connection.getProtocol(); + loader(options); + } + + public void loader(LoadOptions options) { + HugeGraphLoader loader; + try { + loader = new HugeGraphLoader(options); + loader.load(); + } catch (Throwable e) { + Printer.printError("Failed to start loading", e); + return; + } + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/AuditService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/AuditService.java new file mode 100644 index 000000000..aee245edf --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/AuditService.java @@ -0,0 +1,287 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.op; + +import co.elastic.clients.elasticsearch._types.FieldSort; +import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.SortOptionsBuilders; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.TermsQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.Hit; +import co.elastic.clients.elasticsearch.indices.GetAliasRequest; +import co.elastic.clients.json.JsonData; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.entity.op.AuditEntity; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Log4j2 +@Service +public class AuditService extends ESService { + + private final String auditSortKey = "@timestamp"; + private final String sortOrder = "Asc"; + + public IPage queryPage(AuditReq auditReq) throws IOException { + List indexes = new ArrayList<>(); + + List logs = new ArrayList<>(); + int count = 0; + + List services = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(auditReq.services)) { + services.addAll(auditReq.services); + } else { + services.addAll(listServices()); + } + services.forEach(s -> indexes.add(auditIndexName(s))); + + if (CollectionUtils.isNotEmpty(indexes)) { + FieldSort sort = + SortOptionsBuilders.field().field(auditSortKey) + .order(SortOrder.valueOf(sortOrder)).build(); + SortOptions sortKeyOption = + new SortOptions.Builder().field(sort).build(); + + int begine = Math.max(auditReq.pageNo - 1, 0); + List querys = buildESQuery(auditReq); + SearchResponse search = esClient().search((s) -> + s.index(indexes).from(begine * auditReq.pageSize) + .size(auditReq.pageSize) + .query(q -> q.bool(boolQuery -> boolQuery.must(querys)) + ).sort(sortKeyOption), Map.class); + + for (Hit hit: search.hits().hits()) { + String service = hit.index().split("_")[0]; + AuditEntity auditEntity = AuditEntity.fromMap( + HubbleUtil.uncheckedCast(hit.source())); + auditEntity.setService(service); + logs.add(auditEntity); + } + + count = (int) (search.hits().total().value()); + } + + return PageUtil.newPage(logs, auditReq.pageNo, auditReq.pageSize, count); + } + + public List export(AuditReq auditReq) throws IOException { + List indexes = new ArrayList<>(); + + List audits = new ArrayList<>(); + + List services = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(auditReq.services)) { + services.addAll(auditReq.services); + } else { + services.addAll(listServices()); + } + services.forEach(s -> indexes.add(auditIndexName(s))); + + FieldSort sort = + SortOptionsBuilders.field().field(auditSortKey) + .order(SortOrder.valueOf(sortOrder)).build(); + SortOptions sortKeyOption = + new SortOptions.Builder().field(sort).build(); + + List querys = buildESQuery(auditReq); + + int batchSize = maxResultWindow(); + int countLimit = exportCountLimit(); + + int times = (int) Math.ceil((double) countLimit / batchSize); + + for (int i = 0; i < times; i++) { + int start = i * batchSize; + SearchResponse search = esClient().search((s) -> + s.index(indexes).from(start).size(batchSize) + .query(q -> q.bool(boolQuery -> boolQuery.must(querys)) + ).sort(sortKeyOption), Map.class); + + for (Hit hit: search.hits().hits()) { + String service = hit.index().split("_")[0]; + AuditEntity auditEntity = AuditEntity.fromMap( + HubbleUtil.uncheckedCast(hit.source())); + auditEntity.setService(service); + audits.add(auditEntity); + } + + int resultCount = (int) (search.hits().total().value()); + if (resultCount < batchSize) { + break; + } + } + + return audits; + } + + protected List buildESQuery(AuditReq auditReq) { + List querys = new ArrayList<>(); + // start_datetime, end_datetime + if (auditReq.startDatetime != null || auditReq.endDatetime != null) { + Query.Builder builder = new Query.Builder(); + RangeQuery.Builder rBuilder = new RangeQuery.Builder(); + rBuilder = rBuilder.field("@timestamp"); + if (auditReq.startDatetime != null) { + rBuilder = rBuilder.gte(JsonData.of(auditReq.startDatetime)); + } + if (auditReq.endDatetime != null) { + rBuilder = rBuilder.lte(JsonData.of(auditReq.endDatetime)); + } + querys.add(builder.range(rBuilder.build()).build()); + } + + // graphspace + if (!StringUtils.isEmpty(auditReq.graphSpace)) { + Query.Builder builder = new Query.Builder(); + + MatchQuery.Builder mBuilder = new MatchQuery.Builder(); + mBuilder.field("json.audit_graphspace").query(FieldValue.of(auditReq.graphSpace)); + + querys.add(builder.match(mBuilder.build()).build()); + } + + // graph + if (!StringUtils.isEmpty(auditReq.graph)) { + Query.Builder builder = new Query.Builder(); + + MatchQuery.Builder mBuilder = new MatchQuery.Builder(); + mBuilder.field("json.audit_graph").query(FieldValue.of(auditReq.graph)); + + querys.add(builder.match(mBuilder.build()).build()); + } + + // user + if (!StringUtils.isEmpty(auditReq.user)) { + Query.Builder builder = new Query.Builder(); + + MatchQuery.Builder mBuilder = new MatchQuery.Builder(); + mBuilder.field("json.audit_user").query(FieldValue.of(auditReq.user)); + + querys.add(builder.match(mBuilder.build()).build()); + } + + // ip + if (!StringUtils.isEmpty(auditReq.ip)) { + Query.Builder builder = new Query.Builder(); + + MatchQuery.Builder mBuilder = new MatchQuery.Builder(); + mBuilder.field("json.audit_ip").query(FieldValue.of(auditReq.ip)); + + querys.add(builder.match(mBuilder.build()).build()); + } + + // operations + if (CollectionUtils.isNotEmpty(auditReq.operations)) { + Query.Builder builder = new Query.Builder(); + + TermsQuery.Builder tBuilder = new TermsQuery.Builder(); + TermsQueryField.Builder fieldBuilder = new TermsQueryField.Builder(); + fieldBuilder.value(auditReq.operations.stream().map(FieldValue::of) + .collect(Collectors.toList())); + tBuilder.field("json.audit_operation.keyword").terms(fieldBuilder.build()); + + querys.add(builder.terms(tBuilder.build()).build()); + } + + return querys; + } + + @Cacheable(value = "ES_QUERY", key = "#root.targetClass.name+':'+#root" + + ".methodName") + public synchronized List listServices() throws IOException { + Set services = new HashSet<>(); + + GetAliasRequest req = new GetAliasRequest.Builder().index( + auditIndexPattern()).build(); + esClient().indices().getAlias(req).result().keySet() + .stream().filter(x -> !x.startsWith(".")) + .forEach(indexName -> { + String arr1 = indexName.split("_")[0]; + services.add(arr1); + }); + + return services.stream().sorted().collect(Collectors.toList()); + } + + protected String auditIndexPattern() { + return "*_" + logAuditPattern() + "-*"; + } + + protected String auditIndexName(String logType) { + return logType + "_" + logAuditPattern() + "-*"; + } + + @NoArgsConstructor + @AllArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AuditReq { + @JsonProperty("start_datetime") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd " + + "HH:mm:ss", timezone = "GMT+8") + public Date startDatetime; + + @JsonProperty("end_datetime") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd " + + "HH:mm:ss", timezone = "GMT+8") + public Date endDatetime; + + @JsonProperty("user") + public String user; + @JsonProperty("ip") + public String ip; + @JsonProperty("operations") + public List operations; + @JsonProperty("services") + public List services = new ArrayList<>(); + @JsonProperty("graphspace") + public String graphSpace; + @JsonProperty("graph") + public String graph; + @JsonProperty("page_no") + public int pageNo = 1; + @JsonProperty("page_size") + public int pageSize = 20; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/ESService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/ESService.java new file mode 100644 index 000000000..fe50678a6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/ESService.java @@ -0,0 +1,124 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.op; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.util.E; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Arrays; +import java.util.Objects; + +@Log4j2 +public abstract class ESService { + @Autowired + private HugeConfig config; + + public static final String[] LEVELS = new String[]{"TRACE", "OFF", + "FATAL", "ERROR", "WARN", "INFO", "DEBUG"}; + + public static volatile ElasticsearchClient elasticsearchClient; + + public synchronized ElasticsearchClient esClient() { + + if (elasticsearchClient != null) { + return elasticsearchClient; + } + + RestClient restClient = esRestClient(); + + // Create the transport with a Jackson mapper + ElasticsearchTransport transport = new RestClientTransport( + restClient, new JacksonJsonpMapper()); + // And create the API client + elasticsearchClient = new ElasticsearchClient(transport); + + return elasticsearchClient; + } + + protected RestClient esRestClient() { + String esURLS = null; + + // Get monitor.url from system.env + esURLS = System.getenv(HubbleOptions.ES_URL.name()); + if (StringUtils.isEmpty(esURLS)) { + // get monitor.url from file: hugegraph-hubble.properties + esURLS = config.get(HubbleOptions.ES_URL); + } + + E.checkArgument(StringUtils.isNotEmpty(esURLS), + "Please set \"es.urls\" in system environments " + + "or config file(hugegraph-hubble.properties)."); + + String[] esAddresses = esURLS.split(","); + HttpHost[] hosts = Arrays.stream(esAddresses) + .map(HttpHost::create) + .filter(Objects::nonNull) + .toArray(HttpHost[]::new); + log.debug("es.hosts:{}", Arrays.toString(hosts)); + + RestClientBuilder restClientBuidler = RestClient.builder(hosts); + + String esUser = config.get(HubbleOptions.ES_USER); + String esPassword = config.get(HubbleOptions.ES_PASSWORD); + if (StringUtils.isNotEmpty(esUser)) { + final CredentialsProvider credentialsProvider = + new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials( + esUser, + esPassword)); + + restClientBuidler.setHttpClientConfigCallback( + httpClientBuilder -> httpClientBuilder + .setDefaultCredentialsProvider(credentialsProvider) + ); + } + + RestClient restClient = restClientBuidler.build(); + + return restClient; + } + + protected String logAuditPattern() { + return config.get(HubbleOptions.LOG_AUDIT_PATTERN); + } + + protected int exportCountLimit() { + return config.get(HubbleOptions.LOG_EXPORT_COUNT); + } + + protected int maxResultWindow() { + return config.get(HubbleOptions.MAX_RESULT_WINDOW); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LogService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LogService.java new file mode 100644 index 000000000..15476a679 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LogService.java @@ -0,0 +1,301 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.op; + +import co.elastic.clients.elasticsearch._types.FieldSort; +import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.SortOptionsBuilders; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; +import co.elastic.clients.elasticsearch._types.aggregations.Buckets; +import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.TermsQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.Hit; +import co.elastic.clients.elasticsearch.indices.GetAliasResponse; +import co.elastic.clients.json.JsonData; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.entity.op.LogEntity; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class LogService extends ESService { + + protected final String allIndexName = "*"; + + private final String logSortKey = "@timestamp"; + private final String sortOrder = "Asc"; + + public IPage queryPage(LogReq logReq) throws IOException { + + List logs = new ArrayList<>(); + + List indexes = new ArrayList<>(); + // services + List services = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(logReq.services)) { + services.addAll(logReq.services); + } else { + services.addAll(listServices()); + } + services.forEach(s -> indexes.add(s + "-*")); + + List querys = buildQuery(logReq); + + FieldSort sort = + SortOptionsBuilders.field().field(logSortKey) + .order(SortOrder.valueOf(sortOrder)).build(); + SortOptions sortKeyOption = + new SortOptions.Builder().field(sort).build(); + + SearchResponse search = esClient().search((s) -> + s.index(indexes).from(Math.max(logReq.pageNo - 1, 0) * logReq.pageSize) + .size(logReq.pageSize) + .query(q -> q.bool(boolQuery -> + boolQuery.must(querys) + ) + ).sort(sortKeyOption), Map.class); + + for (Hit hit: search.hits().hits()) { + logs.add(LogEntity.fromMap( + HubbleUtil.uncheckedCast(hit.source()))); + } + + return PageUtil.newPage(logs, logReq.pageNo, logReq.pageSize, + (int)(search.hits().total().value())); + } + + public List export(LogReq logReq) throws IOException { + List logs = new ArrayList<>(); + + List indexes = new ArrayList<>(); + // services + List services = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(logReq.services)) { + services.addAll(logReq.services); + } else { + services.addAll(listServices()); + } + services.forEach(s -> indexes.add(s + "-*")); + + List querys = buildQuery(logReq); + + FieldSort sort = + SortOptionsBuilders.field().field(logSortKey) + .order(SortOrder.valueOf(sortOrder)).build(); + SortOptions sortKeyOption = + new SortOptions.Builder().field(sort).build(); + + int batchSize = maxResultWindow(); + int countLimit = exportCountLimit(); + + int times = (int) Math.ceil((double) countLimit / batchSize); + + for (int i = 0; i < times; i++) { + int start = i * batchSize; + + SearchResponse search = esClient().search((s) -> + s.index(indexes).from(start).size(batchSize) + .query(q -> q.bool(boolQuery -> boolQuery.must(querys)) + ).sort(sortKeyOption), Map.class); + + for (Hit hit : search.hits().hits()) { + logs.add(LogEntity.fromMap( + HubbleUtil.uncheckedCast(hit.source()))); + } + + int resultCount = (int) (search.hits().total().value()); + if (resultCount < batchSize) { + break; + } + } + + return logs; + } + + protected List buildQuery(LogReq logReq) { + // 根据Query信息,生成ES的query + + List querys = new ArrayList<>(); + // start_datetime, end_datetime + if (logReq.startDatetime != null || logReq.endDatetime != null) { + Query.Builder builder = new Query.Builder(); + // builder.range(e->e.field()) + RangeQuery.Builder rBuilder = new RangeQuery.Builder(); + rBuilder = rBuilder.field("@timestamp"); + if (logReq.startDatetime != null) { + rBuilder = rBuilder.gte(JsonData.of(logReq.startDatetime)); + } + if (logReq.endDatetime != null) { + rBuilder = rBuilder.lte(JsonData.of(logReq.endDatetime)); + } + querys.add(builder.range(rBuilder.build()).build()); + } + + // query + if (!StringUtils.isEmpty(logReq.query)) { + Query.Builder builder = new Query.Builder(); + + MatchQuery.Builder mBuilder = new MatchQuery.Builder(); + mBuilder.field("message").query(FieldValue.of(logReq.query)); + + querys.add(builder.match(mBuilder.build()).build()); + } + + // hosts + if (CollectionUtils.isNotEmpty(logReq.hosts)) { + Query.Builder builder = new Query.Builder(); + + TermsQuery.Builder tBuilder = new TermsQuery.Builder(); + TermsQueryField.Builder fieldBuilder = new TermsQueryField.Builder(); + fieldBuilder.value(logReq.hosts.stream().map(FieldValue::of) + .collect(Collectors.toList())); + tBuilder.field("host.hostname.keyword").terms(fieldBuilder.build()); + + querys.add(builder.terms(tBuilder.build()).build()); + } + + // Level + if (StringUtils.isNotEmpty(logReq.level)) { + int levelIndex = ArrayUtils.indexOf(LEVELS, + logReq.level.toUpperCase()); + + String[] retianLevels = Arrays.copyOfRange(LEVELS, 0, + levelIndex + 1); + + Query.Builder builder = new Query.Builder(); + + TermsQuery.Builder tBuilder = new TermsQuery.Builder(); + TermsQueryField.Builder fieldBuilder = new TermsQueryField.Builder(); + fieldBuilder.value(Arrays.stream(retianLevels).map(FieldValue::of) + .collect(Collectors.toList())); + tBuilder.field("level.keyword").terms(fieldBuilder.build()); + + querys.add(builder.terms(tBuilder.build()).build()); + } + + return querys; + } + + @Cacheable(value = "ES_QUERY", key = "#root.targetClass.name+':'+#root" + + ".methodName") + public synchronized List listServices() throws IOException { + Set services = new HashSet<>(); + + GetAliasResponse res = esClient().indices().getAlias(); + res.result().keySet().stream() + // filter hidden index and audit index + .filter(x -> !(x.startsWith(".") || x.contains(logAuditPattern()))) + .forEach(indexName -> services.add(indexName.split("-")[0])); + + return services.stream().sorted().collect(Collectors.toList()); + } + + @Cacheable(value = "ES_QUERY", key = "#root.targetClass.name+':'+#root" + + ".methodName") + public List listHosts() throws IOException { + + List hosts = new ArrayList<>(); + + final String hostField = "host.hostname.keyword"; + + return esAggTerms(ImmutableList.of(allIndexName), hostField, 20); + } + + protected List esAggTerms(List indexNames, String field, + int top) throws IOException { + String key = "field_key"; + Aggregation agg = Aggregation.of( + a -> a.terms(v -> v.field(field).size(top))); + + // DO Request + SearchResponse response + = esClient().search((s) -> s.index(indexNames) + .aggregations(key, agg), + Object.class); + + // Get agg terms from response + Buckets buckets = response.aggregations() + .get(key) + .sterms() + .buckets(); + + return buckets.array().stream().map(b -> b.key()) + .collect(Collectors.toList()); + + } + + @NoArgsConstructor + @AllArgsConstructor + public static class LogReq { + @JsonProperty("start_datetime") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd " + + "HH:mm:ss", timezone = "GMT+8") + public Date startDatetime; + + @JsonProperty("end_datetime") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd " + + "HH:mm:ss", timezone = "GMT+8") + public Date endDatetime; + + @JsonProperty("query") + public String query; + + @JsonProperty("level") + public String level; + + @JsonProperty("services") + public List services = new ArrayList<>(); + + @JsonProperty("hosts") + public List hosts = new ArrayList<>(); + + @JsonProperty("page_no") + public int pageNo = 1; + + @JsonProperty("page_size") + public int pageSize = 20; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ApplicationInfoService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ApplicationInfoService.java new file mode 100644 index 000000000..ee411b660 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ApplicationInfoService.java @@ -0,0 +1,55 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.query; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.hugegraph.entity.query.ApplicationInfo; +import org.apache.hugegraph.mapper.query.ApplicationInfoMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ApplicationInfoService { + + @Autowired + public ApplicationInfoMapper mapper; + + public void insertOrUpdateAppInfo(ApplicationInfo appInfo) { + mapper.insertOrUpdateAppInfo(appInfo); + } + + public List query(String graphName) { + return mapper.queryByGraph(graphName); + } + + public List query(String graphName, String appName, String appType) { + return mapper.query(graphName, appName, appType); + } + + public void delete(String graphName, String appName, String appType) { + QueryWrapper query = + new QueryWrapper<>(); + query.eq("graph_name", graphName) + .eq("app_name", appName) + .eq("app_type", appType); + mapper.delete(query); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/EditElementHistoryService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/EditElementHistoryService.java new file mode 100644 index 000000000..34c5a9fc6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/EditElementHistoryService.java @@ -0,0 +1,170 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.query; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.apache.hugegraph.entity.query.ElementEditHistory; +import org.apache.hugegraph.mapper.query.EditElementHistoryMapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.lang3.StringUtils; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class EditElementHistoryService { + + @Autowired + private EditElementHistoryMapper mapper; + + public List queryByLimit(int limit) { + return mapper.queryByLimit(limit); + } + + public int add(ElementEditHistory history) { + return mapper.insert(history); + } + + public int add(List histories) { + try { + return mapper.insertBatch(histories); + } catch (Exception e) { + log.error("add element edit history [{}] error: ", + StringUtils.join(histories, ","), e); + } + return 0; + } + + public int add(String graphspace, String graph, + String elementId, String label, int propertyNum, + String optionType, Date optionTime, + String optionPerson, String content) { + ElementEditHistory eleEditHis = ElementEditHistory.builder() + .graphspace(graphspace) + .graph(graph) + .elementId(elementId) + .label(label) + .propertyNum(propertyNum) + .optionType(optionType) + .optionTime(optionTime) + .optionPerson(optionPerson) + .content(content) + .build(); + try { + return mapper.insert(eleEditHis); + } catch (Exception e) { + log.error("add element edit history [{}] error: ", + eleEditHis.toString(), e); + } + return 0; + } + + public IPage list(String graphSpace, + String graph, + int current, + int pageSize) { + QueryWrapper query = Wrappers.query(); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .orderByDesc("option_time"); + Page page = new Page<>(current, pageSize); + return this.mapper.selectPage(page, query); + } + + public IPage queryByConditions(String graphSpace, + String graph, + List optionPersons, + List optionTypes, + String elementId, + String optionTimeFrom, + String optionTimeTo, + int current, + int pageSize) { + QueryWrapper query = Wrappers.query(); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .orderByDesc("option_time"); + + if (!elementId.isEmpty()) { + query.eq("element_id", elementId); + } + + if (!optionPersons.isEmpty()) { + query.in("option_person", optionPersons); + } + + if (!optionTypes.isEmpty()) { + query.in("option_type", optionTypes); + } + + if (!(StringUtils.isEmpty(optionTimeFrom) && + StringUtils.isEmpty(optionTimeTo))) { + query.between("option_time", optionTimeFrom, optionTimeTo); + } + + Page page = new Page<>(current, pageSize); + return this.mapper.selectPage(page, query); + } + + public List queryByElementId(String graphSpace, + String graph, + String elementId) { + return mapper.queryByElementId(graphSpace, graph, elementId); + } + + public Map queryByElementIds(String graphSpace, + String graph, + List elementIds) { + List list = + mapper.queryByElementIds(graphSpace, graph, elementIds); + Map map = new HashMap<>(); + list.forEach(ele -> { + map.merge(ele.getElementId(), ele, (oldValue, newValue) -> { + // 如果 newValue 的 optionTime 比较晚,则替换原来的元素 + return newValue.getOptionTime().compareTo(oldValue.getOptionTime()) > 0 ? + newValue : oldValue; + }); + }); + return map; + } + + public IPage queryByElementId(String graphSpace, + String graph, + String elementId, + int current, + int pageSize) { + QueryWrapper query = Wrappers.query(); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("element_id", elementId) + .orderByDesc("option_time"); + Page page = new Page<>(current, pageSize); + return this.mapper.selectPage(page, query); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java index c6fffac7e..5239967f6 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java @@ -21,6 +21,15 @@ import java.time.Instant; import java.time.temporal.ChronoField; +import org.apache.hugegraph.util.Ex; +import com.google.common.collect.ImmutableMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.enums.AsyncTaskStatus; @@ -29,16 +38,8 @@ import org.apache.hugegraph.exception.InternalException; import org.apache.hugegraph.mapper.query.ExecuteHistoryMapper; import org.apache.hugegraph.options.HubbleOptions; -import org.apache.hugegraph.service.HugeClientPoolService; import org.apache.hugegraph.structure.Task; import org.apache.hugegraph.util.HubbleUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Transactional; - import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -54,17 +55,28 @@ public class ExecuteHistoryService { private HugeConfig config; @Autowired private ExecuteHistoryMapper mapper; - @Autowired - private HugeClientPoolService poolService; - - private HugeClient getClient(int connId) { - return this.poolService.getOrCreate(connId); - } - public IPage list(int connId, long current, long pageSize) { - HugeClient client = this.getClient(connId); + /** + * 查询执行历史记录 + * + * @param client HugeClient客户端 + * @param type 查询类型 + * @param current 当前页码 + * @param pageSize 每页记录数 + * @param text2Gremlin 是否需要展示text2Gremlin的执行历史记录 + * @return IPage 执行历史记录分页结果 + */ + public IPage list(HugeClient client, int type, long current, + long pageSize, boolean text2Gremlin) { + this.checkTypeValid(type); QueryWrapper query = Wrappers.query(); - query.eq("conn_id", connId).orderByDesc("create_time"); + query.eq("graphspace", client.getGraphSpaceName()) + .eq("graph", client.getGraphName()) + .in("execute_type", ExecuteType.getMatchedTypes(type)) + .orderByDesc("create_time"); + if (text2Gremlin) { + query.apply("LENGTH(text) > 0"); + } Page page = new Page<>(current, pageSize); IPage results = this.mapper.selectPage(page, query); @@ -88,12 +100,34 @@ public IPage list(int connId, long current, long pageSize) { p.setAsyncStatus(AsyncTaskStatus.UNKNOWN); } } + if (p.getType().equals(ExecuteType.CYPHER_ASYNC)) { + try { + Task task = client.task().get(p.getAsyncId()); + long endDate = task.updateTime() > 0 ? task.updateTime() : + now.getLong(ChronoField.INSTANT_SECONDS); + p.setDuration(endDate - task.createTime()); + p.setAsyncStatus(task.status().toUpperCase()); + } catch (Exception e) { + p.setDuration(0L); + p.setAsyncStatus(AsyncTaskStatus.UNKNOWN); + } + } }); return results; } - public ExecuteHistory get(int connId, int id) { - HugeClient client = this.getClient(connId); + private void checkTypeValid(int type) { + Ex.check(type == ExecuteType.CYPHER.getValue() || + type == ExecuteType.GREMLIN.getValue() || + type == ExecuteType.ALGORITHM.getValue() || + type == ExecuteType.GREMLIN_ASYNC.getValue() || + type == ExecuteType.CYPHER_ASYNC.getValue() || + type == ExecuteType.GREMLIN_ALL.getValue() || + type == ExecuteType.CYPHER_ALL.getValue(), + "executehistory type invalid"); + } + + public ExecuteHistory get(HugeClient client, int id) { ExecuteHistory history = this.mapper.selectById(id); if (history.getType().equals(ExecuteType.GREMLIN_ASYNC)) { try { @@ -123,9 +157,8 @@ public void update(ExecuteHistory history) { } @Transactional(isolation = Isolation.READ_COMMITTED) - public void remove(int connId, int id) { + public void remove(HugeClient client, int id) { ExecuteHistory history = this.mapper.selectById(id); - HugeClient client = this.getClient(connId); if (history.getType().equals(ExecuteType.GREMLIN_ASYNC)) { client.task().delete(history.getAsyncId()); } @@ -141,4 +174,10 @@ public void removeExceedLimit() { int limit = this.config.get(HubbleOptions.EXECUTE_HISTORY_SHOW_LIMIT); this.mapper.deleteExceedLimit(limit); } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public void deleteByGraph(String graphSpace, String graph) { + this.mapper.deleteByMap(ImmutableMap.of("graphspace", graphSpace, + "graph", graph)); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java index 3ece3623a..66afe73ac 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java @@ -18,16 +18,18 @@ package org.apache.hugegraph.service.query; -import org.apache.hugegraph.entity.query.GremlinCollection; -import org.apache.hugegraph.exception.InternalException; -import org.apache.hugegraph.mapper.query.GremlinCollectionMapper; -import org.apache.hugegraph.util.SQLUtil; +import com.google.common.collect.ImmutableMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.GremlinCollection; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.mapper.query.GremlinCollectionMapper; +import org.apache.hugegraph.util.SQLUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -39,60 +41,87 @@ public class GremlinCollectionService { @Autowired private GremlinCollectionMapper mapper; - public IPage list(int connId, String content, + public IPage list(HugeClient client, String content, + String type, Boolean nameOrderAsc, Boolean timeOrderAsc, long current, long pageSize) { QueryWrapper query = Wrappers.query(); + String graphSpace = client.getGraphSpaceName(); + String graph = client.getGraphName(); IPage page = new Page<>(current, pageSize); if (!StringUtils.isEmpty(content)) { // Select by content String value = SQLUtil.escapeLike(content); if (nameOrderAsc != null) { // order by name - assert timeOrderAsc == null; - query.eq("conn_id", connId).and(wrapper -> { - wrapper.like("name", value).or().like("content", value); - }); + checkSingleOrder(nameOrderAsc, timeOrderAsc); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("type", type).and(wrapper -> { + wrapper.like("name", value).or() + .like("content", value); + }); query.orderBy(true, nameOrderAsc, "name"); return this.mapper.selectPage(page, query); } else if (timeOrderAsc != null) { // order by time - assert nameOrderAsc == null; - query.eq("conn_id", connId).and(wrapper -> { - wrapper.like("name", value).or().like("content", value); - }); + checkSingleOrder(nameOrderAsc, timeOrderAsc); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("type", type).and(wrapper -> { + wrapper.like("name", value).or() + .like("content", value); + }); query.orderBy(true, timeOrderAsc, "create_time"); return this.mapper.selectPage(page, query); } else { // order by relativity - assert nameOrderAsc == null && timeOrderAsc == null; - return this.mapper.selectByContentInPage(page, connId, content); + checkSingleOrder(nameOrderAsc, timeOrderAsc); + return this.mapper.selectByContentInPage(page, graphSpace, graph, + content, type); } } else { // Select all if (nameOrderAsc != null) { // order by name - assert timeOrderAsc == null; - query.eq("conn_id", connId).orderBy(true, nameOrderAsc, "name"); + checkSingleOrder(nameOrderAsc, timeOrderAsc); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("type", type) + .orderBy(true, nameOrderAsc, "name"); return this.mapper.selectPage(page, query); } else { // order by time boolean isAsc = timeOrderAsc != null && timeOrderAsc; - query.eq("conn_id", connId).orderBy(true, isAsc, "create_time"); + query.eq("graphspace", graphSpace) + .eq("graph", graph) + .eq("type", type) + .orderBy(true, isAsc, "create_time"); return this.mapper.selectPage(page, query); } } } + private static void checkSingleOrder(Boolean nameOrderAsc, + Boolean timeOrderAsc) { + if (nameOrderAsc != null && timeOrderAsc != null) { + throw new InternalException("Only one gremlin collection order " + + "field can be specified"); + } + } + public GremlinCollection get(int id) { return this.mapper.selectById(id); } - public GremlinCollection getByName(int connId, String name) { + public GremlinCollection getByName(String graphSpace, String graph, + String name, String type) { QueryWrapper query = Wrappers.query(); - query.eq("conn_id", connId); + query.eq("graphspace", graphSpace); + query.eq("graph", graph); query.eq("name", name); + query.eq("type", type); return this.mapper.selectOne(query); } @@ -120,4 +149,10 @@ public void remove(int id) { throw new InternalException("entity.delete.failed", id); } } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public void deleteByGraph(String graphSpace, String graph) { + this.mapper.deleteByMap(ImmutableMap.of("graphspace", graphSpace, + "graph", graph)); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinQueryService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinQueryService.java deleted file mode 100644 index 359a0bd66..000000000 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinQueryService.java +++ /dev/null @@ -1,508 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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 org.apache.hugegraph.service.query; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.api.gremlin.GremlinRequest; -import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.driver.HugeClient; -import org.apache.hugegraph.entity.query.AdjacentQuery; -import org.apache.hugegraph.entity.query.GraphView; -import org.apache.hugegraph.entity.query.GremlinQuery; -import org.apache.hugegraph.entity.query.GremlinResult; -import org.apache.hugegraph.entity.query.GremlinResult.Type; -import org.apache.hugegraph.entity.query.JsonView; -import org.apache.hugegraph.entity.query.TableView; -import org.apache.hugegraph.entity.query.TypedResult; -import org.apache.hugegraph.entity.schema.VertexLabelEntity; -import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.exception.IllegalGremlinException; -import org.apache.hugegraph.exception.InternalException; -import org.apache.hugegraph.exception.ServerException; -import org.apache.hugegraph.options.HubbleOptions; -import org.apache.hugegraph.rest.ClientException; -import org.apache.hugegraph.service.HugeClientPoolService; -import org.apache.hugegraph.service.schema.VertexLabelService; -import org.apache.hugegraph.structure.constant.Direction; -import org.apache.hugegraph.structure.constant.IdStrategy; -import org.apache.hugegraph.structure.graph.Edge; -import org.apache.hugegraph.structure.graph.Path; -import org.apache.hugegraph.structure.graph.Vertex; -import org.apache.hugegraph.structure.gremlin.Result; -import org.apache.hugegraph.structure.gremlin.ResultSet; -import org.apache.hugegraph.util.Ex; -import org.apache.hugegraph.util.GremlinUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; - -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@Service -public class GremlinQueryService { - - private static final Set ILLEGAL_GREMLIN_EXCEPTIONS = ImmutableSet.of( - "groovy.lang.MissingPropertyException", - "groovy.lang.MissingMethodException", - "groovy.lang.MissingFieldException", - "groovy.lang.MissingClassException", - "groovy.lang.IncorrectClosureArgumentsException", - "org.codehaus.groovy.control.CompilationFailedException", - "org.codehaus.groovy.control.MultipleCompilationErrorsException", - "org.codehaus.groovy.runtime.metaclass.MethodSelectionException" - ); - - private static final String TIMEOUT_EXCEPTION = - "java.net.SocketTimeoutException"; - - private static final String CONN_REFUSED_MSG = "Connection refused"; - - @Autowired - private HugeConfig config; - @Autowired - private HugeClientPoolService poolService; - @Autowired - private VertexLabelService vlService; - - private HugeClient getClient(int connId) { - return this.poolService.getOrCreate(connId); - } - - public GremlinResult executeQuery(int connId, GremlinQuery query) { - HugeClient client = this.getClient(connId); - - log.debug("The original gremlin ==> {}", query.getContent()); - String gremlin = this.optimize(query.getContent()); - log.debug("The optimized gremlin ==> {}", gremlin); - // Execute gremlin query - ResultSet resultSet = this.executeGremlin(gremlin, client); - // Scan data, vote the result type - TypedResult typedResult = this.parseResults(resultSet); - // Build json view - JsonView jsonView = new JsonView(typedResult.getData()); - // Build table view - TableView tableView = this.buildTableView(typedResult); - // Build graph view - GraphView graphView = this.buildGraphView(typedResult, client); - return GremlinResult.builder() - .type(typedResult.getType()) - .jsonView(jsonView) - .tableView(tableView) - .graphView(graphView) - .build(); - } - - public Long executeAsyncTask(int connId, GremlinQuery query) { - HugeClient client = this.getClient(connId); - - log.debug("The async gremlin ==> {}", query.getContent()); - // Execute optimized gremlin query - GremlinRequest request = new GremlinRequest(query.getContent()); - return client.gremlin().executeAsTask(request); - } - - public GremlinResult expandVertex(int connId, AdjacentQuery query) { - HugeClient client = this.getClient(connId); - - // Build gremlin query - String gremlin = this.buildGremlinQuery(connId, query); - log.debug("expand vertex gremlin ==> {}", gremlin); - // Execute gremlin query - ResultSet resultSet = this.executeGremlin(gremlin, client); - - List vertices = new ArrayList<>(resultSet.size()); - List edges = new ArrayList<>(resultSet.size()); - for (Iterator iter = resultSet.iterator(); iter.hasNext(); ) { - Path path = iter.next().getPath(); - List objects = path.objects(); - assert objects.size() == 3; - Edge edge = (Edge) objects.get(1); - Vertex vertex = (Vertex) objects.get(2); - // Filter vertices and edges that existed in query - if (query.retainEdge(edge)) { - edges.add(edge); - } - if (query.retainVertex(vertex)) { - vertices.add(vertex); - } - } - // Build graph view - GraphView graphView = new GraphView(vertices, edges); - return GremlinResult.builder() - .type(Type.PATH) - .graphView(graphView) - .build(); - } - - private String optimize(String content) { - // Optimize each gremlin statemnts - int limit = this.config.get(HubbleOptions.GREMLIN_SUFFIX_LIMIT); - String[] originalParts = StringUtils.split(content, ";"); - String[] optimizeParts = new String[originalParts.length]; - for (int i = 0; i < originalParts.length; i++) { - String part = originalParts[i]; - optimizeParts[i] = GremlinUtil.optimizeLimit(part, limit); - } - return StringUtils.join(optimizeParts, ";"); - } - - private ResultSet executeGremlin(String gremlin, HugeClient client) { - try { - return client.gremlin().gremlin(gremlin).execute(); - } catch (ServerException e) { - String exception = e.exception(); - log.error("Gremlin execute failed: {}", exception); - if (ILLEGAL_GREMLIN_EXCEPTIONS.contains(exception)) { - throw new IllegalGremlinException("gremlin.illegal-statemnt", e, - e.message()); - } - throw new ExternalException("gremlin.execute.failed", e, e.message()); - } catch (ClientException e) { - Throwable cause = e.getCause(); - if (cause != null) { - String message = cause.getMessage(); - if (message != null && message.startsWith(TIMEOUT_EXCEPTION)) { - throw new InternalException("gremlin.execute.timeout", e, - message); - } - if (message != null && message.contains(CONN_REFUSED_MSG)) { - throw new InternalException("gremlin.connection.refused", e, - message); - } - } - throw e; - } catch (Exception e) { - log.error("Gremlin execute failed", e); - throw new ExternalException("gremlin.execute.failed", e, - e.getMessage()); - } - } - - private TypedResult parseResults(ResultSet resultSet) { - if (resultSet == null) { - return new TypedResult(Type.EMPTY, null); - } - Iterator iter = resultSet.iterator(); - if (!iter.hasNext()) { - return new TypedResult(Type.EMPTY, null); - } - - Map typeVotes = new HashMap<>(); - List typedData = new ArrayList<>(resultSet.size()); - while (iter.hasNext()) { - Result result = iter.next(); - if (result == null) { - // NOTE: null value doesn't vote - continue; - } - Object object = result.getObject(); - Type type; - if (object instanceof Vertex) { - type = Type.VERTEX; - } else if (object instanceof Edge) { - type = Type.EDGE; - } else if (object instanceof Path) { - type = Type.PATH; - } else { - type = Type.GENERAL; - } - typeVotes.compute(type, (k, v) -> v == null ? 1 : v + 1); - typedData.add(object); - } - - Type type; - if (typeVotes.isEmpty()) { - type = Type.EMPTY; - } else { - // Find the key with max value - type = Collections.max(typeVotes.entrySet(), - Comparator.comparingInt(Map.Entry::getValue)) - .getKey(); - } - return new TypedResult(type, typedData); - } - - private TableView buildTableView(TypedResult typedResult) { - List data = typedResult.getData(); - if (CollectionUtils.isEmpty(data)) { - return TableView.EMPTY; - } - - switch (typedResult.getType()) { - case EMPTY: - return TableView.EMPTY; - case GENERAL: - // result - List results = new ArrayList<>(data.size()); - data.forEach(object -> { - results.add(ImmutableMap.of("result", object)); - }); - return new TableView(TableView.GENERAL_HEADER, results); - case VERTEX: - // id, label, properties - List vertices = new ArrayList<>(data.size()); - data.forEach(object -> { - if (object instanceof Vertex) { - vertices.add(object); - } - }); - return new TableView(TableView.VERTEX_HEADER, vertices); - case EDGE: - // id, label, source, target, properties - List edges = new ArrayList<>(data.size()); - data.forEach(object -> { - if (object instanceof Edge) { - edges.add(object); - } - }); - return new TableView(TableView.EDGE_HEADER, edges); - case PATH: - // path, only fill vertex/edge id - List paths = new ArrayList<>(data.size()); - data.forEach(object -> { - if (object instanceof Path) { - Path path = (Path) object; - List ids = new ArrayList<>(); - path.objects().forEach(element -> { - if (element instanceof Vertex) { - ids.add(((Vertex) element).id()); - } else if (element instanceof Edge) { - ids.add(((Edge) element).id()); - } else { - ids.add(element); - } - }); - paths.add(ImmutableMap.of("path", ids)); - } - }); - return new TableView(TableView.PATH_HEADER, paths); - default: - throw new AssertionError(String.format( - "Unknown result type '%s'", typedResult.getType())); - } - } - - private GraphView buildGraphView(TypedResult result, HugeClient client) { - List data = result.getData(); - if (!result.getType().isGraph() || CollectionUtils.isEmpty(data)) { - return GraphView.EMPTY; - } - - Map vertices = new HashMap<>(); - Map edges = new HashMap<>(); - for (Object object : data) { - if (object instanceof Vertex) { - Vertex vertex = (Vertex) object; - vertices.put(vertex.id(), vertex); - } else if (object instanceof Edge) { - Edge edge = (Edge) object; - edges.put(edge.id(), edge); - } else if (object instanceof Path) { - List elements = ((Path) object).objects(); - for (Object element : elements) { - if (element instanceof Vertex) { - Vertex vertex = (Vertex) element; - vertices.put(vertex.id(), vertex); - } else if (element instanceof Edge) { - Edge edge = (Edge) element; - edges.put(edge.id(), edge); - } else { - return GraphView.EMPTY; - } - } - } - } - - if (!edges.isEmpty()) { - if (vertices.isEmpty()) { - vertices = this.verticesOfEdge(result, edges, client); - } else { - // TODO: reduce the number of requests - vertices.putAll(this.verticesOfEdge(result, edges, client)); - } - } else { - if (!vertices.isEmpty()) { - edges = this.edgesOfVertex(result, vertices, client); - } - } - - if (!edges.isEmpty()) { - Ex.check(!vertices.isEmpty(), - "gremlin.edges.linked-vertex.not-exist"); - } - return new GraphView(vertices.values(), edges.values()); - } - - private String buildGremlinQuery(int connId, AdjacentQuery query) { - int degreeLimit = this.config.get( - HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT); - - Object id = this.getRealVertexId(connId, query); - StringBuilder sb = new StringBuilder("g.V("); - // vertex id - sb.append(GremlinUtil.escapeId(id)).append(")"); - // direction - String direction = query.getDirection() != null ? - query.getDirection().name() : - Direction.BOTH.name(); - sb.append(".toE(").append(direction); - // edge label - if (query.getEdgeLabel() != null) { - sb.append(", '").append(query.getEdgeLabel()).append("')"); - } else { - sb.append(")"); - } - if (query.getConditions() != null) { - // properties - for (AdjacentQuery.Condition condition : query.getConditions()) { - // key - sb.append(".has('").append(condition.getKey()).append("', "); - // value - sb.append(condition.getOperator()).append("(") - .append(GremlinUtil.escape(condition.getValue())).append(")"); - sb.append(")"); - } - } - // limit - sb.append(".limit(").append(degreeLimit).append(")"); - // other vertex - sb.append(".otherV().path()"); - return sb.toString(); - } - - private Object getRealVertexId(int connId, AdjacentQuery query) { - VertexLabelEntity entity = this.vlService.get(query.getVertexLabel(), - connId); - IdStrategy idStrategy = entity.getIdStrategy(); - String rawVertexId = query.getVertexId(); - try { - if (idStrategy == IdStrategy.AUTOMATIC || - idStrategy == IdStrategy.CUSTOMIZE_NUMBER) { - return Long.parseLong(rawVertexId); - } else if (idStrategy == IdStrategy.CUSTOMIZE_UUID) { - return UUID.fromString(rawVertexId); - } - } catch (Exception e) { - throw new ExternalException("gremlin.convert-vertex-id.failed", e, - rawVertexId, idStrategy); - } - - assert idStrategy == IdStrategy.PRIMARY_KEY || - idStrategy == IdStrategy.CUSTOMIZE_STRING; - return rawVertexId; - } - - private Map edgesOfVertex(TypedResult result, - Map vertices, - HugeClient client) { - final HugeConfig config = this.config; - int batchSize = config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS); - int edgeLimit = config.get(HubbleOptions.GREMLIN_EDGES_TOTAL_LIMIT); - int degreeLimit = config.get(HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT); - - Set vertexIds = vertices.keySet(); - Map edges = new HashMap<>(vertexIds.size()); - Iterables.partition(vertexIds, batchSize).forEach(batch -> { - List escapedIds = batch.stream() - .map(GremlinUtil::escapeId) - .collect(Collectors.toList()); - String ids = StringUtils.join(escapedIds, ","); - // Any better way to find two vertices has linked? - String gremlin; - if (result.getType().isPath()) { - // If result type is path, the vertices count not too much in theory - gremlin = String.format("g.V(%s).bothE().local(limit(%s)).dedup()", - ids, degreeLimit); - } else { - gremlin = String.format("g.V(%s).bothE().dedup().limit(%s)", - ids, edgeLimit); - } - ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); - // The edges count for per vertex - Map degrees = new HashMap<>(resultSet.size()); - for (Iterator iter = resultSet.iterator(); iter.hasNext(); ) { - Edge edge = iter.next().getEdge(); - Object source = edge.sourceId(); - Object target = edge.targetId(); - // only add the interconnected edges of the found vertices - if (!vertexIds.contains(source) || !vertexIds.contains(target)) { - continue; - } - edges.put(edge.id(), edge); - if (edges.size() >= edgeLimit) { - break; - } - - int deg = degrees.compute(source, (k, v) -> v == null ? 1 : v + 1); - if (deg >= degreeLimit) { - break; - } - deg = degrees.compute(target, (k, v) -> v == null ? 1 : v + 1); - if (deg >= degreeLimit) { - break; - } - } - }); - return edges; - } - - private Map verticesOfEdge(TypedResult result, - Map edges, - HugeClient client) { - int batchSize = this.config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS); - - Set vertexIds = new HashSet<>(edges.size() * 2); - edges.values().forEach(edge -> { - vertexIds.add(edge.sourceId()); - vertexIds.add(edge.targetId()); - }); - - Map vertices = new HashMap<>(vertexIds.size()); - Iterables.partition(vertexIds, batchSize).forEach(batch -> { - List escapedIds = batch.stream() - .map(GremlinUtil::escapeId) - .collect(Collectors.toList()); - String ids = StringUtils.join(escapedIds, ","); - String gremlin = String.format("g.V(%s)", ids); - ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); - for (Iterator iter = resultSet.iterator(); iter.hasNext(); ) { - Vertex vertex = iter.next().getVertex(); - vertices.put(vertex.id(), vertex); - } - }); - return vertices; - } -} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/QueryService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/QueryService.java new file mode 100644 index 000000000..f99461e98 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/QueryService.java @@ -0,0 +1,605 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.query; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.api.gremlin.GremlinRequest; +// TODO fix import +//import org.apache.hugegraph.client.api.gremlin.GremlinRequest; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.AdjacentQuery; +import org.apache.hugegraph.entity.query.GraphView; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.entity.query.GremlinResult; +import org.apache.hugegraph.entity.query.JsonView; +import org.apache.hugegraph.entity.query.TableView; +import org.apache.hugegraph.entity.query.TypedResult; +import org.apache.hugegraph.entity.query.GremlinResult.Type; +import org.apache.hugegraph.entity.schema.VertexLabelEntity; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.exception.IllegalGremlinException; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.exception.ServerException; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.rest.ClientException; +import org.apache.hugegraph.service.schema.VertexLabelService; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.IdStrategy; +import org.apache.hugegraph.structure.graph.Edge; +import org.apache.hugegraph.structure.graph.Path; +import org.apache.hugegraph.structure.graph.Vertex; +import org.apache.hugegraph.structure.gremlin.Result; +import org.apache.hugegraph.structure.gremlin.ResultSet; +import org.apache.hugegraph.util.Ex; +import org.apache.hugegraph.util.GremlinUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Log4j2 +@Service +public class QueryService { + + private static final Set ILLEGAL_GREMLIN_EXCEPTIONS = ImmutableSet.of( + "groovy.lang.MissingPropertyException", + "groovy.lang.MissingMethodException", + "groovy.lang.MissingFieldException", + "groovy.lang.MissingClassException", + "groovy.lang.IncorrectClosureArgumentsException", + "org.codehaus.groovy.control.CompilationFailedException", + "org.codehaus.groovy.control.MultipleCompilationErrorsException", + "org.codehaus.groovy.runtime.metaclass.MethodSelectionException" + ); + + public static final Set CONDITION_OPERATORS = ImmutableSet.of( + "eq", "gt", "gte", "lt", "lte", "neq" + ); + + private static final String TIMEOUT_EXCEPTION = + "java.net.SocketTimeoutException"; + + private static final String CONN_REFUSED_MSG = "Connection refused"; + + @Autowired + private HugeConfig config; + @Autowired + private VertexLabelService vlService; + + public ResultSet executeQueryCount(HugeClient client, String query) { + + log.debug("The original gremlin ==> {}", query); + // Execute gremlin query + ResultSet resultSet = this.executeGremlin(query, client); + return resultSet; + } + + public GremlinResult executeGremlinQuery(HugeClient client, GremlinQuery query) { + + log.debug("The original gremlin ==> {}", query.getContent()); + String gremlin = this.optimize(query.getContent()); + log.debug("The optimized gremlin ==> {}", gremlin); + // Execute gremlin query + ResultSet resultSet = this.executeGremlin(gremlin, client); + // Scan data, vote the result type + TypedResult typedResult = this.parseResults(resultSet); + // Build json view + JsonView jsonView = new JsonView(typedResult.getData()); + // Build table view + TableView tableView = this.buildTableView(typedResult); + // Build graph view + GraphView graphView = this.buildGraphView(typedResult, client); + return GremlinResult.builder() + .type(typedResult.getType()) + .jsonView(jsonView) + .tableView(tableView) + .graphView(graphView) + .build(); + } + + public JsonView executeSingleGremlinQuery(HugeClient client, GremlinQuery query) { + + log.debug("The original gremlin ==> {}", query.getContent()); + String gremlin = this.optimize(query.getContent()); + log.debug("The optimized gremlin ==> {}", gremlin); + // Execute gremlin query + ResultSet resultSet = this.executeGremlin(gremlin, client); + // Scan data, vote the result type + TypedResult typedResult = this.parseResults(resultSet); + // Build json view + JsonView jsonView = new JsonView(typedResult.getData()); + return jsonView; + } + + public GremlinResult executeCypherQuery(HugeClient client, String query) { + // Execute cypher query + ResultSet resultSet = this.executeCypher(query, client); + // Scan data, vote the result type + TypedResult typedResult = this.parseResults(resultSet); + // Build json view + JsonView jsonView = new JsonView(typedResult.getData()); + // Build table view + TableView tableView = this.buildTableView(typedResult); + // Build graph view + GraphView graphView = this.buildGraphView(typedResult, client); + return GremlinResult.builder() + .type(typedResult.getType()) + .jsonView(jsonView) + .tableView(tableView) + .graphView(graphView) + .build(); + } + + private ResultSet executeCypher(String cypher, HugeClient client) { + try { + return client.cypher().cypher(cypher); + } catch (ServerException e) { + String exception = e.exception(); + log.error("Gremlin execute failed: {}", exception); + if (ILLEGAL_GREMLIN_EXCEPTIONS.contains(exception)) { + throw new IllegalGremlinException("gremlin.illegal-statemnt", e, + e.message()); + } + throw new ExternalException("gremlin.execute.failed", e, e.message()); + } catch (ClientException e) { + Throwable cause = e.getCause(); + if (cause != null) { + String message = cause.getMessage(); + if (message != null && message.startsWith(TIMEOUT_EXCEPTION)) { + throw new InternalException("gremlin.execute.timeout", e, + message); + } + if (message != null && message.contains(CONN_REFUSED_MSG)) { + throw new InternalException("gremlin.connection.refused", e, + message); + } + } + throw e; + } catch (Exception e) { + log.error("Gremlin execute failed", e); + throw new ExternalException("gremlin.execute.failed", e, + e.getMessage()); + } + } + + public Long executeGremlinAsyncTask(HugeClient client, GremlinQuery query) { + + log.debug("The async gremlin ==> {}", query.getContent()); + // Execute optimized gremlin query + GremlinRequest request = new GremlinRequest(query.getContent()); + return client.gremlin().executeAsTask(request); + } + + public Long executeCypherAsyncTask(HugeClient client, String query) { + + log.debug("The async gremlin ==> {}", query); + // Execute optimized gremlin query + return client.cypher().executeAsTask(query); + } + + public GremlinResult expandVertex(HugeClient client, AdjacentQuery query) { + + // Build gremlin query + String gremlin = this.buildGremlinQuery(client, query); + log.debug("expand vertex gremlin ==> {}", gremlin); + // Execute gremlin query + ResultSet resultSet = this.executeGremlin(gremlin, client); + + List vertices = new ArrayList<>(resultSet.size()); + List edges = new ArrayList<>(resultSet.size()); + for (Iterator iter = resultSet.iterator(); iter.hasNext();) { + Path path = iter.next().getPath(); + List objects = path.objects(); + Ex.check(objects.size() == 3, + "The expanded vertex path must contain 3 objects, " + + "but got %s", objects.size()); + Edge edge = (Edge) objects.get(1); + Vertex vertex = (Vertex) objects.get(2); + // Filter vertices and edges that existed in query + if (query.retainEdge(edge)) { + edges.add(edge); + } + if (query.retainVertex(vertex)) { + vertices.add(vertex); + } + } + // Build graph view + GraphView graphView = new GraphView(vertices, edges); + return GremlinResult.builder() + .type(Type.PATH) + .graphView(graphView) + .build(); + } + + private String optimize(String content) { + // Optimize each gremlin statemnts + int limit = this.config.get(HubbleOptions.GREMLIN_SUFFIX_LIMIT); + String[] originalParts = StringUtils.split(content, ";"); + String[] optimizeParts = new String[originalParts.length]; + for (int i = 0; i < originalParts.length; i++) { + String part = originalParts[i]; + optimizeParts[i] = GremlinUtil.optimizeLimit(part, limit); + } + return StringUtils.join(optimizeParts, ";"); + } + + private ResultSet executeGremlin(String gremlin, HugeClient client) { + try { + return client.gremlin().gremlin(gremlin).execute(); + } catch (ServerException e) { + String exception = e.exception(); + log.error("Gremlin execute failed: {}", exception); + if (ILLEGAL_GREMLIN_EXCEPTIONS.contains(exception)) { + throw new IllegalGremlinException("gremlin.illegal-statemnt", e, + e.message()); + } + throw new ExternalException("gremlin.execute.failed", e, e.message()); + } catch (ClientException e) { + Throwable cause = e.getCause(); + if (cause != null) { + String message = cause.getMessage(); + if (message != null && message.startsWith(TIMEOUT_EXCEPTION)) { + throw new InternalException("gremlin.execute.timeout", e, + message); + } + if (message != null && message.contains(CONN_REFUSED_MSG)) { + throw new InternalException("gremlin.connection.refused", e, + message); + } + } + throw e; + } catch (Exception e) { + log.error("Gremlin execute failed", e); + throw new ExternalException("gremlin.execute.failed", e, + e.getMessage()); + } + } + + private TypedResult parseResults(ResultSet resultSet) { + if (resultSet == null) { + return new TypedResult(Type.EMPTY, null); + } + Iterator iter = resultSet.iterator(); + if (!iter.hasNext()) { + return new TypedResult(Type.EMPTY, null); + } + + Map typeVotes = new HashMap<>(); + List typedData = new ArrayList<>(resultSet.size()); + while (iter.hasNext()) { + Result result = iter.next(); + if (result == null) { + // NOTE: null value doesn't vote + continue; + } + Object object = result.getObject(); + Type type; + if (object instanceof Vertex) { + type = Type.VERTEX; + } else if (object instanceof Edge) { + type = Type.EDGE; + } else if (object instanceof Path) { + type = Type.PATH; + } else { + type = Type.GENERAL; + } + typeVotes.compute(type, (k, v) -> v == null ? 1 : v + 1); + typedData.add(object); + } + + Type type; + if (typeVotes.isEmpty()) { + type = Type.EMPTY; + } else { + // Find the key with max value + type = Collections.max(typeVotes.entrySet(), + Comparator.comparingInt(Map.Entry::getValue)) + .getKey(); + } + return new TypedResult(type, typedData); + } + + private TableView buildTableView(TypedResult typedResult) { + List data = typedResult.getData(); + if (CollectionUtils.isEmpty(data)) { + return TableView.EMPTY; + } + + switch (typedResult.getType()) { + case EMPTY: + return TableView.EMPTY; + case GENERAL: + // result + List results = new ArrayList<>(data.size()); + data.forEach(object -> { + results.add(ImmutableMap.of("result", object)); + }); + return new TableView(TableView.GENERAL_HEADER, results); + case VERTEX: + // id, label, properties + List vertices = new ArrayList<>(data.size()); + data.forEach(object -> { + if (object instanceof Vertex) { + vertices.add(object); + } + }); + return new TableView(TableView.VERTEX_HEADER, vertices); + case EDGE: + // id, label, source, target, properties + List edges = new ArrayList<>(data.size()); + data.forEach(object -> { + if (object instanceof Edge) { + edges.add(object); + } + }); + return new TableView(TableView.EDGE_HEADER, edges); + case PATH: + // path, only fill vertex/edge id + List paths = new ArrayList<>(data.size()); + data.forEach(object -> { + if (object instanceof Path) { + Path path = (Path) object; + List ids = new ArrayList<>(); + path.objects().forEach(element -> { + if (element instanceof Vertex) { + ids.add(((Vertex) element).id()); + } else if (element instanceof Edge) { + ids.add(((Edge) element).id()); + } else { + ids.add(element); + } + }); + paths.add(ImmutableMap.of("path", ids)); + } + }); + return new TableView(TableView.PATH_HEADER, paths); + default: + throw new AssertionError(String.format( + "Unknown result type '%s'", typedResult.getType())); + } + } + + private GraphView buildGraphView(TypedResult result, HugeClient client) { + List data = result.getData(); + if (!result.getType().isGraph() || CollectionUtils.isEmpty(data)) { + return GraphView.EMPTY; + } + + Map vertices = new HashMap<>(); + Map edges = new HashMap<>(); + for (Object object : data) { + if (object instanceof Vertex) { + Vertex vertex = (Vertex) object; + vertices.put(vertex.id(), vertex); + } else if (object instanceof Edge) { + Edge edge = (Edge) object; + edges.put(edge.id(), edge); + } else if (object instanceof Path) { + List elements = ((Path) object).objects(); + for (Object element : elements) { + if (element instanceof Vertex) { + Vertex vertex = (Vertex) element; + vertices.put(vertex.id(), vertex); + } else if (element instanceof Edge) { + Edge edge = (Edge) element; + edges.put(edge.id(), edge); + } else { + return GraphView.EMPTY; + } + } + } + } + + if (!edges.isEmpty()) { + if (vertices.isEmpty()) { + vertices = this.verticesOfEdge(result, edges, client); + } else { + // TODO: reduce the number of requests + vertices.putAll(this.verticesOfEdge(result, edges, client)); + } + } else { + if (!vertices.isEmpty()) { + edges = this.edgesOfVertex(result, vertices, client); + } + } + + if (!edges.isEmpty()) { + Ex.check(!vertices.isEmpty(), + "gremlin.edges.linked-vertex.not-exist"); + } + return new GraphView(vertices.values(), edges.values()); + } + + private String buildGremlinQuery(HugeClient client, AdjacentQuery query) { + int degreeLimit = this.config.get( + HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT); + + Object id = this.getRealVertexId(client, query); + StringBuilder sb = new StringBuilder("g.V("); + // vertex id + sb.append(GremlinUtil.escapeId(id)).append(")"); + // direction + String direction = query.getDirection() != null ? + query.getDirection().name() : + Direction.BOTH.name(); + sb.append(".toE(").append(direction); + // edge label + if (query.getEdgeLabel() != null) { + sb.append(", ").append(GremlinUtil.escape(query.getEdgeLabel())) + .append(")"); + } else { + sb.append(")"); + } + if (query.getConditions() != null) { + // properties + for (AdjacentQuery.Condition condition : query.getConditions()) { + // key + sb.append(".has(").append(GremlinUtil.escape(condition.getKey())) + .append(", "); + // value + sb.append(this.checkConditionOperator(condition.getOperator())) + .append("(") + .append(GremlinUtil.escape(this.checkConditionValue( + condition.getValue()))) + .append(")"); + sb.append(")"); + } + } + // limit + sb.append(".limit(").append(degreeLimit).append(")"); + // other vertex + sb.append(".otherV().path()"); + return sb.toString(); + } + + private String checkConditionOperator(String operator) { + Ex.check(CONDITION_OPERATORS.contains(operator), + "common.param.should-belong-to", "condition.operator", + CONDITION_OPERATORS); + return operator; + } + + private Object checkConditionValue(Object value) { + Ex.check(value instanceof String || value instanceof Number || + value instanceof Boolean, + "common.param.should-belong-to", "condition.value", + "String/Number/Boolean"); + return value; + } + + private Object getRealVertexId(HugeClient client, AdjacentQuery query) { + VertexLabelEntity entity = this.vlService.get(query.getVertexLabel(), + client); + IdStrategy idStrategy = entity.getIdStrategy(); + String rawVertexId = query.getVertexId(); + try { + if (idStrategy == IdStrategy.AUTOMATIC || + idStrategy == IdStrategy.CUSTOMIZE_NUMBER) { + return Long.parseLong(rawVertexId); + } else if (idStrategy == IdStrategy.CUSTOMIZE_UUID) { + return UUID.fromString(rawVertexId); + } + } catch (Exception e) { + throw new ExternalException("gremlin.convert-vertex-id.failed", e, + rawVertexId, idStrategy); + } + + Ex.check(idStrategy == IdStrategy.PRIMARY_KEY || + idStrategy == IdStrategy.CUSTOMIZE_STRING, + "Unsupported vertex id strategy: %s", idStrategy); + return rawVertexId; + } + + private Map edgesOfVertex(TypedResult result, + Map vertices, + HugeClient client) { + final HugeConfig config = this.config; + int batchSize = config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS); + int edgeLimit = config.get(HubbleOptions.GREMLIN_EDGES_TOTAL_LIMIT); + int degreeLimit = config.get(HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT); + + Set vertexIds = vertices.keySet(); + Map edges = new HashMap<>(vertexIds.size()); + Iterables.partition(vertexIds, batchSize).forEach(batch -> { + List escapedIds = batch.stream() + .map(GremlinUtil::escapeId) + .collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + // Any better way to find two vertices has linked? + String gremlin; + if (result.getType().isPath()) { + // If result type is path, the vertices count not too much in theory + gremlin = String.format("g.V(%s).bothE().local(limit(%s)).dedup()", + ids, degreeLimit); + } else { + gremlin = String.format("g.V(%s).bothE().dedup().limit(%s)", + ids, edgeLimit); + } + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + // The edges count for per vertex + Map degrees = new HashMap<>(resultSet.size()); + for (Iterator iter = resultSet.iterator(); iter.hasNext();) { + Edge edge = iter.next().getEdge(); + Object source = edge.sourceId(); + Object target = edge.targetId(); + // only add the interconnected edges of the found vertices + if (!vertexIds.contains(source) || !vertexIds.contains(target)) { + continue; + } + edges.put(edge.id(), edge); + if (edges.size() >= edgeLimit) { + break; + } + + int deg = degrees.compute(source, (k, v) -> v == null ? 1 : v + 1); + if (deg >= degreeLimit) { + break; + } + deg = degrees.compute(target, (k, v) -> v == null ? 1 : v + 1); + if (deg >= degreeLimit) { + break; + } + } + }); + return edges; + } + + private Map verticesOfEdge(TypedResult result, + Map edges, + HugeClient client) { + int batchSize = this.config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS); + + Set vertexIds = new HashSet<>(edges.size() * 2); + edges.values().forEach(edge -> { + vertexIds.add(edge.sourceId()); + vertexIds.add(edge.targetId()); + }); + + Map vertices = new HashMap<>(vertexIds.size()); + Iterables.partition(vertexIds, batchSize).forEach(batch -> { + List escapedIds = batch.stream() + .map(GremlinUtil::escapeId) + .collect(Collectors.toList()); + String ids = StringUtils.join(escapedIds, ","); + String gremlin = String.format("g.V(%s)", ids); + ResultSet resultSet = client.gremlin().gremlin(gremlin).execute(); + for (Iterator iter = resultSet.iterator(); iter.hasNext();) { + Vertex vertex = iter.next().getVertex(); + vertices.put(vertex.id(), vertex); + } + }); + return vertices; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/saas/PrometheusService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/saas/PrometheusService.java new file mode 100644 index 000000000..131a2290d --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/saas/PrometheusService.java @@ -0,0 +1,178 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.saas; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.Response; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.rest.AbstractRestClient; +import org.apache.hugegraph.rest.RestResult; +import org.apache.hugegraph.util.HubbleUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Requests metrics data stored by Prometheus. + */ + +@Slf4j +@Service +public class PrometheusService { + + private static final String Q_PATH = "/api/v1/query"; + private static final String Q_RANGE_PATH = "api/v1/query_range"; + + @Autowired + private HugeConfig config; + + private MetricsClient client; + + public PrometheusService() { + } + + public PrometheusService(String prometheusUrl) { + client = new MetricsClient(prometheusUrl, 30000); + } + + private MetricsClient getClient() { + if (client == null) { + client = new MetricsClient(config.get(HubbleOptions.PROMETHEUS_URL), + 30000); + } + return client; + } + + public long queryByDelta(String query, long start, long end) { + log.info("queryByDelta: query=[{}] start=[{}], end=[{}]", query, start, end); + return queryByTimestamp(query, end) - queryByTimestamp(query, start); + } + + /** + * 根据时间戳查询当前的统计数据 + * + * @param query promSQL查询语句 + * @param timestamp 时间戳 + * @return 返回查询结果的Map对象 + */ + private long queryByTimestamp(String query, long timestamp) { + Map map = new HashMap<>(); + map.put("query", encodeUrl(query)); + map.put("time", timestamp); + RestResult result = getClient().get(Q_PATH, map); + return extractCount(result.readObject(Map.class)); + } + + /** + * 根据指定范围查询数据 + * + * @param query 查询条件 + * @param from 起始时间戳 + * @param to 结束时间戳 + * @param step 时间步长/s (比如(to-from)=10s, step=2s, 则返回5组数据) + * @return 返回查询结果 + */ + public List> queryByRange(String query, long from, long to, long step) { + log.info("queryByDelta: query=[{}] start=[{}], end=[{}], step=[{}]", query, from, to, step); + Map map = new HashMap<>(); + map.put("query", encodeUrl(query)); + map.put("start", from); + map.put("end", to); + map.put("step", step); + RestResult result = getClient().get(Q_RANGE_PATH, map); + return extractDistribution( + HubbleUtil.uncheckedCast(result.readObject(Map.class))); + + } + + private List> extractDistribution(Map map) { + List> values = null; + try { + Map map1 = + HubbleUtil.uncheckedCast(map.get("data")); + List list = HubbleUtil.uncheckedCast(map1.get("result")); + Map map2 = HubbleUtil.uncheckedCast(list.get(0)); + values = HubbleUtil.uncheckedCast(map2.get("values")); + } catch (Exception e) { + log.error("extract distribution from prometheus response error"); + } + return values; + } + + /** + * 统计当前日期往前一天的请求量 + * + * @param lastDay eg: 2023-12-06 23:59:59 + * @return + */ + public Long queryCountOffSet1Day(String lastDay) { + long[] timestamps = HubbleUtil.getTimestampsBefore24Hours(lastDay); + String query = "sum(nginx_vts_filter_requests_total{})"; + return queryByDelta(query, timestamps[0], timestamps[1]); + } + + private long extractCount(Map dataMap) { + long count = 0L; + try { + Map map1 = (Map) dataMap.get("data"); + Map map2 = (Map) ((List) map1.get("result")).get(0); + List list = (List) map2.get("value"); + + for (int i = 0; i < list.size(); i++) { + Object obj = list.get(i); + if (obj instanceof String) { + String countS = (String) obj; + count = Long.valueOf(countS.split("\\.")[0]); + break; + } + } + } catch (Exception e) { + log.error("extract count from prometheus response error"); + } + + return count; + } + + private static String encodeUrl(String url) { + // promQL 语法中括号不需要转义 + String encodeUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); + return encodeUrl.replaceAll("%28", "(") + .replaceAll("%29", ")") + .replaceAll("\\+", "%20"); + } + + private static class MetricsClient extends AbstractRestClient { + + public MetricsClient(String url, int timeout) { + super(url, timeout); + } + + @Override + protected void checkStatus(Response response, int... ints) { + + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/EdgeLabelService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/EdgeLabelService.java index a43d9dc2d..3edd32a7d 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/EdgeLabelService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/EdgeLabelService.java @@ -30,6 +30,11 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.schema.ConflictCheckEntity; @@ -46,6 +51,7 @@ import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.exception.ServerException; import org.apache.hugegraph.structure.SchemaElement; +import org.apache.hugegraph.structure.constant.EdgeLabelType; import org.apache.hugegraph.structure.constant.Frequency; import org.apache.hugegraph.structure.schema.EdgeLabel; import org.apache.hugegraph.structure.schema.IndexLabel; @@ -53,9 +59,6 @@ import org.apache.hugegraph.structure.schema.VertexLabel; import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.JsonUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import lombok.extern.log4j.Log4j2; @@ -70,17 +73,17 @@ public class EdgeLabelService extends SchemaService { @Autowired private PropertyIndexService piService; - public List list(int connId) { - return this.list(Collections.emptyList(), connId); + public List list(HugeClient client) { + return this.list(Collections.emptyList(), client); } - public List list(Collection names, int connId) { - return this.list(names, connId, true); + public List list(Collection names, + HugeClient client) { + return this.list(names, client, true, null); } - public List list(Collection names, int connId, - boolean emptyAsAll) { - HugeClient client = this.client(connId); + public List list(Collection names, HugeClient client, + boolean emptyAsAll, String type) { List edgeLabels; if (CollectionUtils.isEmpty(names)) { if (emptyAsAll) { @@ -91,21 +94,51 @@ public List list(Collection names, int connId, } else { edgeLabels = client.schema().getEdgeLabels(new ArrayList<>(names)); } + List edgeLabelsQuery = client.schema().getEdgeLabels(); List indexLabels = client.schema().getIndexLabels(); List results = new ArrayList<>(edgeLabels.size()); edgeLabels.forEach(edgeLabel -> { - results.add(convert(edgeLabel, indexLabels)); + if (StringUtils.isEmpty(type) || + (StringUtils.isNotEmpty(type) && + edgeLabel.edgeLabelType() == EdgeLabelType.valueOf(type))) { + EdgeLabelEntity entity = + convert(edgeLabel, indexLabels, client); + + if (edgeLabel.parent()) { + List children = new ArrayList<>(); + for (EdgeLabel edgeLabel1: edgeLabelsQuery) { + if (edgeLabel.name().equals(edgeLabel1.parentLabel())) { + children.add(edgeLabel1.name()); + } + } + entity.setChildren(children); + } + + results.add(entity); + } }); return results; } - public EdgeLabelEntity get(String name, int connId) { - HugeClient client = this.client(connId); + public EdgeLabelEntity get(String name, HugeClient client) { try { EdgeLabel edgeLabel = client.schema().getEdgeLabel(name); List indexLabels = client.schema().getIndexLabels(); - return convert(edgeLabel, indexLabels); + + List edgeLabelsQuery = client.schema().getEdgeLabels(); + EdgeLabelEntity entity = convert(edgeLabel, indexLabels, client); + + if (edgeLabel.parent()) { + List children = new ArrayList<>(); + for (EdgeLabel edgeLabel1: edgeLabelsQuery) { + if (edgeLabel.name().equals(edgeLabel1.parentLabel())) { + children.add(edgeLabel1.name()); + } + } + entity.setChildren(children); + } + return entity; } catch (ServerException e) { if (e.status() == Constant.STATUS_NOT_FOUND) { throw new ExternalException("schema.edgelabel.not-exist", @@ -115,14 +148,14 @@ public EdgeLabelEntity get(String name, int connId) { } } - public void checkExist(String name, int connId) { + public void checkExist(String name, HugeClient client) { // Throw exception if it doesn't exist - this.get(name, connId); + this.get(name, client); } - public void checkNotExist(String name, int connId) { + public void checkNotExist(String name, HugeClient client) { try { - this.get(name, connId); + this.get(name, client); } catch (ExternalException e) { Throwable cause = e.getCause(); if (cause instanceof ServerException && @@ -134,8 +167,7 @@ public void checkNotExist(String name, int connId) { throw new ExternalException("schema.edgelabel.exist", name); } - public void add(EdgeLabelEntity entity, int connId) { - HugeClient client = this.client(connId); + public void add(EdgeLabelEntity entity, HugeClient client) { EdgeLabel edgeLabel = convert(entity, client); try { client.schema().addEdgeLabel(edgeLabel); @@ -147,8 +179,7 @@ public void add(EdgeLabelEntity entity, int connId) { this.piService.addBatch(indexLabels, client); } - public void update(EdgeLabelUpdateEntity entity, int connId) { - HugeClient client = this.client(connId); + public void update(EdgeLabelUpdateEntity entity, HugeClient client) { EdgeLabel edgeLabel = convert(entity, client); // All existed indexlabels @@ -157,8 +188,8 @@ public void update(EdgeLabelUpdateEntity entity, int connId) { List addedIndexLabelNames = entity.getAppendPropertyIndexNames(); List addedIndexLabels = convertIndexLabels( - entity.getAppendPropertyIndexes(), - client, false, entity.getName()); + entity.getAppendPropertyIndexes(), + client, false, entity.getName()); List removedIndexLabelNames = entity.getRemovePropertyIndexes(); @@ -166,8 +197,8 @@ public void update(EdgeLabelUpdateEntity entity, int connId) { for (String name : addedIndexLabelNames) { if (existedIndexLabelNames.contains(name)) { throw new ExternalException( - "schema.edgelabel.update.append-index-existed", - entity.getName(), name); + "schema.edgelabel.update.append-index-existed", + entity.getName(), name); } } } @@ -175,8 +206,8 @@ public void update(EdgeLabelUpdateEntity entity, int connId) { for (String name : removedIndexLabelNames) { if (!existedIndexLabelNames.contains(name)) { throw new ExternalException( - "schema.edgelabel.update.remove-index-unexisted", - entity.getName(), name); + "schema.edgelabel.update.remove-index-unexisted", + entity.getName(), name); } } } @@ -192,29 +223,29 @@ public void update(EdgeLabelUpdateEntity entity, int connId) { this.piService.removeBatch(removedIndexLabelNames, client); } - public void remove(String name, int connId) { - HugeClient client = this.client(connId); + public void remove(String name, HugeClient client) { client.schema().removeEdgeLabelAsync(name); } public ConflictDetail checkConflict(ConflictCheckEntity entity, - int connId, boolean compareEachOther) { + HugeClient client, + boolean compareEachOther) { ConflictDetail detail = new ConflictDetail(SchemaType.EDGE_LABEL); if (CollectionUtils.isEmpty(entity.getElEntities())) { return detail; } Map originElEntities = new HashMap<>(); - for (EdgeLabelEntity e : this.list(connId)) { + for (EdgeLabelEntity e : this.list(client)) { originElEntities.put(e.getName(), e); } this.pkService.checkConflict(entity.getPkEntities(), detail, - connId, compareEachOther); + client, compareEachOther); this.piService.checkConflict(entity.getPiEntities(), detail, - connId, compareEachOther); + client, compareEachOther); this.vlService.checkConflict(entity.getVlEntities(), detail, - connId, compareEachOther); + client, compareEachOther); for (EdgeLabelEntity elEntity : entity.getElEntities()) { // Firstly check if any properties are conflicted if (detail.anyPropertyKeyConflict(elEntity.getPropNames())) { @@ -233,7 +264,7 @@ public ConflictDetail checkConflict(ConflictCheckEntity entity, } // Then check conflict of edge label itself EdgeLabelEntity originElEntity = originElEntities.get( - elEntity.getName()); + elEntity.getName()); ConflictStatus status = SchemaEntity.compare(elEntity, originElEntity); detail.add(elEntity, status); @@ -244,9 +275,8 @@ public ConflictDetail checkConflict(ConflictCheckEntity entity, return detail; } - public void reuse(ConflictDetail detail, int connId) { + public void reuse(ConflictDetail detail, HugeClient client) { Ex.check(!detail.hasConflict(), "schema.cannot-reuse-conflict"); - HugeClient client = this.client(connId); List propertyKeys = this.pkService.filter(detail, client); if (!propertyKeys.isEmpty()) { @@ -315,17 +345,20 @@ public void removeBatch(List edgeLabels, HugeClient client) { removeBatch(names, client, func, SchemaType.EDGE_LABEL); } - private static EdgeLabelEntity convert(EdgeLabel edgeLabel, - List indexLabels) { + private EdgeLabelEntity convert(EdgeLabel edgeLabel, + List indexLabels, + HugeClient client) { if (edgeLabel == null) { return null; } - Set properties = collectProperties(edgeLabel); + Set properties = collectProperties(edgeLabel, client); List propertyIndexes = collectPropertyIndexes(edgeLabel, indexLabels); boolean linkMultiTimes = edgeLabel.frequency() == Frequency.MULTIPLE; return EdgeLabelEntity.builder() .name(edgeLabel.name()) + .parentLabel(edgeLabel.parentLabel()) + .edgeLabelType(edgeLabel.edgeLabelType().string()) .sourceLabel(edgeLabel.sourceLabel()) .targetLabel(edgeLabel.targetLabel()) .linkMultiTimes(linkMultiTimes) @@ -338,34 +371,35 @@ private static EdgeLabelEntity convert(EdgeLabel edgeLabel, .build(); } - private static EdgeLabelStyle getStyle(SchemaElement element) { - String styleValue = (String) element.userdata().get(USER_KEY_STYLE); - if (styleValue != null) { - return JsonUtil.fromJson(styleValue, EdgeLabelStyle.class); - } else { - return new EdgeLabelStyle(); - } - } - private static EdgeLabel convert(EdgeLabelEntity entity, HugeClient client) { if (entity == null) { return null; } Frequency frequency = entity.isLinkMultiTimes() ? Frequency.MULTIPLE : - Frequency.SINGLE; + Frequency.SINGLE; EdgeLabelStyle style = entity.getStyle(); - return client.schema().edgeLabel(entity.getName()) - .sourceLabel(entity.getSourceLabel()) - .targetLabel(entity.getTargetLabel()) - .frequency(frequency) - .properties(toStringArray(entity.getPropNames())) - .sortKeys(toStringArray(entity.getSortKeys())) - .nullableKeys(toStringArray(entity.getNullableProps())) - .enableLabelIndex(entity.isOpenLabelIndex()) - .userdata(USER_KEY_CREATE_TIME, entity.getCreateTime()) - .userdata(USER_KEY_STYLE, JsonUtil.toJson(style)) - .build(); + EdgeLabel.Builder builder = + client.schema().edgeLabel(entity.getName()); + if (EdgeLabelType.valueOf(entity.getEdgeLabelType()).parent()) { + builder.asBase(); + } else { + builder.sourceLabel(entity.getSourceLabel()) + .targetLabel(entity.getTargetLabel()) + .frequency(frequency) + .properties(toStringArray(entity.getPropNames())) + .sortKeys(toStringArray(entity.getSortKeys())) + .nullableKeys(toStringArray(entity.getNullableProps())) + .enableLabelIndex(entity.getOpenLabelIndex()) + .userdata(USER_KEY_CREATE_TIME, entity.getCreateTime()) + .userdata(USER_KEY_STYLE, JsonUtil.toJson(style)); + } + + if (EdgeLabelType.valueOf(entity.getEdgeLabelType()).sub()) { + builder.withBase(entity.getParentLabel()); + } + + return builder.build(); } private static EdgeLabel convert(EdgeLabelUpdateEntity entity, @@ -393,4 +427,13 @@ private static EdgeLabel convert(EdgeLabelUpdateEntity entity, } return edgeLabel; } + + private static EdgeLabelStyle getStyle(SchemaElement element) { + String styleValue = (String) element.userdata().get(USER_KEY_STYLE); + if (styleValue != null) { + return JsonUtil.fromJson(styleValue, EdgeLabelStyle.class); + } else { + return new EdgeLabelStyle(); + } + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/GroovySchemaCompatibility.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/GroovySchemaCompatibility.java new file mode 100644 index 000000000..05903ea19 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/GroovySchemaCompatibility.java @@ -0,0 +1,339 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.schema; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +import org.apache.commons.lang3.StringUtils; + +import org.apache.hugegraph.driver.SchemaManager; +import org.apache.hugegraph.structure.SchemaElement; +import org.apache.hugegraph.structure.constant.AggregateType; +import org.apache.hugegraph.structure.constant.Cardinality; +import org.apache.hugegraph.structure.constant.DataType; +import org.apache.hugegraph.structure.constant.Frequency; +import org.apache.hugegraph.structure.constant.HugeType; +import org.apache.hugegraph.structure.constant.IdStrategy; +import org.apache.hugegraph.structure.constant.IndexType; +import org.apache.hugegraph.structure.constant.WriteType; +import org.apache.hugegraph.structure.schema.EdgeLabel; +import org.apache.hugegraph.structure.schema.IndexLabel; +import org.apache.hugegraph.structure.schema.PropertyKey; +import org.apache.hugegraph.structure.schema.SchemaLabel; +import org.apache.hugegraph.structure.schema.VertexLabel; + +/** + * Keeps Hubble Schema export compatible with Servers which return structured + * Schema JSON for the historical {@code format=groovy} request. + */ +public final class GroovySchemaCompatibility { + + private static final String PREFIX = "graph.schema()"; + + private GroovySchemaCompatibility() { + } + + public static String export(SchemaManager schema) { + String legacy = schema.getGroovySchema(); + if (StringUtils.isNotEmpty(legacy)) { + return legacy; + } + + StringBuilder result = new StringBuilder(); + schema.getPropertyKeys().stream().sorted(GroovySchemaCompatibility::byName) + .forEach(value -> append(result, property(value))); + schema.getVertexLabels().stream().sorted(GroovySchemaCompatibility::byName) + .forEach(value -> append(result, vertex(value))); + schema.getEdgeLabels().stream().sorted(GroovySchemaCompatibility::byName) + .forEach(value -> append(result, edge(value))); + schema.getIndexLabels().stream().sorted(GroovySchemaCompatibility::byName) + .forEach(value -> append(result, index(value))); + return result.toString(); + } + + private static void append(StringBuilder result, String line) { + if (result.length() > 0) { + result.append('\n'); + } + result.append(line); + } + + private static String property(PropertyKey value) { + StringBuilder line = builder("propertyKey", value.name()); + line.append(dataType(value.dataType())); + if (value.cardinality() == Cardinality.LIST) { + line.append(".valueList()"); + } else if (value.cardinality() == Cardinality.SET) { + line.append(".valueSet()"); + } + if (value.aggregateType() != null && + value.aggregateType() != AggregateType.NONE) { + line.append(".calc") + .append(capitalize(value.aggregateType().string())).append("()"); + } + if (value.writeType() != null && value.writeType() != WriteType.OLTP) { + line.append(".writeType(").append(quote(value.writeType().name())) + .append(")"); + } + appendUserdata(line, value); + return finish(line); + } + + private static String vertex(VertexLabel value) { + StringBuilder line = builder("vertexLabel", value.name()); + IdStrategy strategy = value.idStrategy(); + if (strategy == IdStrategy.AUTOMATIC) { + line.append(".useAutomaticId()"); + } else if (strategy == IdStrategy.PRIMARY_KEY) { + line.append(".usePrimaryKeyId()"); + } else if (strategy == IdStrategy.CUSTOMIZE_STRING) { + line.append(".useCustomizeStringId()"); + } else if (strategy == IdStrategy.CUSTOMIZE_NUMBER) { + line.append(".useCustomizeNumberId()"); + } else if (strategy == IdStrategy.CUSTOMIZE_UUID) { + line.append(".useCustomizeUuidId()"); + } + appendStrings(line, "properties", value.properties()); + appendStrings(line, "primaryKeys", value.primaryKeys()); + appendStrings(line, "nullableKeys", value.nullableKeys()); + appendTtl(line, value.ttl(), value.ttlStartTime()); + appendLabelIndex(line, value); + appendUserdata(line, value); + return finish(line); + } + + private static String edge(EdgeLabel value) { + StringBuilder line = builder("edgeLabel", value.name()); + if (value.parent()) { + line.append(".asBase()"); + } else if (value.sub()) { + line.append(".withBase(").append(quote(value.parentLabel())).append(")"); + } else { + if (value.general()) { + line.append(".asGeneral()"); + } + value.links().forEach(link -> link.forEach((source, target) -> + line.append(".link(").append(quote(source)).append(", ") + .append(quote(target)).append(")"))); + } + appendStrings(line, "properties", value.properties()); + appendStrings(line, "sortKeys", value.sortKeys()); + appendStrings(line, "nullableKeys", value.nullableKeys()); + if (value.frequency() == Frequency.SINGLE) { + line.append(".singleTime()"); + } else if (value.frequency() == Frequency.MULTIPLE) { + line.append(".multiTimes()"); + } + appendTtl(line, value.ttl(), value.ttlStartTime()); + appendLabelIndex(line, value); + appendUserdata(line, value); + return finish(line); + } + + private static String index(IndexLabel value) { + StringBuilder line = builder("indexLabel", value.name()); + if (value.baseType() == HugeType.VERTEX_LABEL) { + line.append(".onV(").append(quote(value.baseValue())).append(")"); + } else { + line.append(".onE(").append(quote(value.baseValue())).append(")"); + } + appendStrings(line, "by", value.indexFields()); + IndexType type = value.indexType(); + if (type != null) { + line.append('.').append(type.string()).append("()"); + } + if (!value.rebuild()) { + line.append(".rebuild(false)"); + } + appendUserdata(line, value); + return finish(line); + } + + private static StringBuilder builder(String type, String name) { + return new StringBuilder(PREFIX).append('.').append(type).append('(') + .append(quote(name)).append(')'); + } + + private static String finish(StringBuilder line) { + return line.append(".ifNotExist().create();").toString(); + } + + private static void appendStrings(StringBuilder line, String method, + Collection values) { + if (values == null || values.isEmpty()) { + return; + } + StringJoiner args = new StringJoiner(", "); + values.forEach(value -> args.add(quote(value))); + line.append('.').append(method).append('(').append(args).append(')'); + } + + private static void appendTtl(StringBuilder line, long ttl, + String ttlStartTime) { + if (ttl > 0L) { + line.append(".ttl(").append(ttl).append(')'); + } + if (StringUtils.isNotEmpty(ttlStartTime)) { + line.append(".ttlStartTime(").append(quote(ttlStartTime)).append(')'); + } + } + + private static void appendLabelIndex(StringBuilder line, + SchemaLabel value) { + try { + line.append(".enableLabelIndex(") + .append(value.enableLabelIndex()).append(')'); + } catch (NullPointerException ignored) { + // Older Server responses may omit the nullable option; builder + // default is the only faithful representation in that case. + } + } + + private static void appendUserdata(StringBuilder line, + SchemaElement value) { + value.userdata().entrySet().stream() + .sorted(Map.Entry.comparingByKey()).forEach(entry -> { + line.append(".userdata(").append(quote(entry.getKey())) + .append(", ").append(literal(entry.getValue())).append(')'); + }); + } + + private static String dataType(DataType type) { + if (type == null) { + return ""; + } + switch (type) { + case TEXT: return ".asText()"; + case INT: return ".asInt()"; + case DATE: return ".asDate()"; + case UUID: return ".asUUID()"; + case BOOLEAN: return ".asBoolean()"; + case BYTE: return ".asByte()"; + case BLOB: return ".asBlob()"; + case DOUBLE: return ".asDouble()"; + case FLOAT: return ".asFloat()"; + case LONG: return ".asLong()"; + default: + return ".dataType(org.apache.hugegraph.structure.constant." + + "DataType." + type.name() + ")"; + } + } + + private static String literal(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof Number || value instanceof Boolean) { + return value.toString(); + } + if (value instanceof Collection) { + StringJoiner values = new StringJoiner(", ", "[", "]"); + ((Collection) value).forEach(item -> values.add(literal(item))); + return values.toString(); + } + if (value instanceof Map) { + if (((Map) value).isEmpty()) { + return "[:]"; + } + StringJoiner values = new StringJoiner(", ", "[", "]"); + ((Map) value).forEach((key, item) -> + values.add(quote(String.valueOf(key)) + ": " + + literal(item))); + return values.toString(); + } + if (value.getClass().isArray()) { + StringJoiner values = new StringJoiner(", ", "[", "]"); + for (int index = 0; index < Array.getLength(value); index++) { + values.add(literal(Array.get(value, index))); + } + return values.toString(); + } + if (value instanceof CharSequence || value instanceof Character || + value instanceof Enum) { + return quote(value.toString()); + } + throw new IllegalArgumentException("Unsupported userdata type: " + + value.getClass().getName()); + } + + public static List splitStatements(String content) { + List statements = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + char quote = 0; + boolean escaped = false; + for (int index = 0; index < content.length(); index++) { + char value = content.charAt(index); + if (quote != 0) { + current.append(value); + if (escaped) { + escaped = false; + } else if (value == '\\') { + escaped = true; + } else if (value == quote) { + quote = 0; + } + continue; + } + if (value == '\'' || value == '"') { + quote = value; + current.append(value); + } else if (value == ';' || value == '\n' || value == '\r') { + addStatement(statements, current); + } else { + current.append(value); + } + } + if (quote != 0 || escaped) { + throw new IllegalArgumentException("Unterminated Groovy string"); + } + addStatement(statements, current); + return statements; + } + + private static void addStatement(List statements, + StringBuilder current) { + if (current.length() > 0) { + statements.add(current.toString()); + current.setLength(0); + } + } + + private static String quote(String value) { + if (value == null) { + return "null"; + } + return "'" + value.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\r", "\\r") + .replace("\n", "\\n") + "'"; + } + + private static String capitalize(String value) { + return Character.toUpperCase(value.charAt(0)) + value.substring(1); + } + + private static int byName(SchemaElement left, SchemaElement right) { + return left.name().compareTo(right.name()); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyIndexService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyIndexService.java index 774d42df5..61b0e0a99 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyIndexService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyIndexService.java @@ -28,6 +28,10 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.schema.ConflictDetail; @@ -40,10 +44,6 @@ import org.apache.hugegraph.structure.constant.HugeType; import org.apache.hugegraph.structure.schema.IndexLabel; import org.apache.hugegraph.util.PageUtil; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.extern.log4j.Log4j2; @@ -52,17 +52,17 @@ @Service public class PropertyIndexService extends SchemaService { - public List list(int connId) { - return this.list(Collections.emptyList(), connId); + public List list(HugeClient client) { + return this.list(Collections.emptyList(), client); } - public List list(Collection names, int connId) { - return this.list(names, connId, true); + public List list(Collection names, + HugeClient client) { + return this.list(names, client, true); } - public List list(Collection names, int connId, + public List list(Collection names, HugeClient client, boolean emptyAsAll) { - HugeClient client = this.client(connId); List indexLabels; if (CollectionUtils.isEmpty(names)) { if (emptyAsAll) { @@ -80,9 +80,8 @@ public List list(Collection names, int connId, return results; } - public IPage list(int connId, HugeType type, + public IPage list(HugeClient client, HugeType type, int pageNo, int pageSize) { - HugeClient client = this.client(connId); List indexLabels = client.schema().getIndexLabels(); List results = new ArrayList<>(); @@ -112,18 +111,17 @@ public IPage list(int connId, HugeType type, * --------------+------------------------+--------------------------------- * xxxname | xxxByName | name * --------------+------------------------+--------------------------------- - * | personByName | name + * | personByName | name * person +------------------------+--------------------------------- - * | personByAgeAndName | age name + * | personByAgeAndName | age name * --------------+------------------------+--------------------------------- - * | softwareByName | name + * | softwareByName | name * software +------------------------+--------------------------------- - * | softwareByPriveAndName | price name + * | softwareByPriveAndName | price name * --------------+------------------------+--------------------------------- */ - public IPage list(int connId, HugeType type, String content, - int pageNo, int pageSize) { - HugeClient client = this.client(connId); + public IPage list(HugeClient client, HugeType type, + String content, int pageNo, int pageSize) { List indexLabels = client.schema().getIndexLabels(); Map> matchedResults = new HashMap<>(); @@ -138,10 +136,10 @@ public IPage list(int connId, HugeType type, String content, boolean match = baseValue.contains(content); if (match) { groupedIndexes = matchedResults.computeIfAbsent(baseValue, - k -> new ArrayList<>()); + k -> new ArrayList<>()); } else { groupedIndexes = unMatchResults.computeIfAbsent(baseValue, - k -> new ArrayList<>()); + k -> new ArrayList<>()); } match = match || indexLabel.name().contains(content) || indexLabel.indexFields().stream() @@ -154,12 +152,11 @@ public IPage list(int connId, HugeType type, String content, // Sort matched results by relevance if (!StringUtils.isEmpty(content)) { for (Map.Entry> entry : - matchedResults.entrySet()) { + matchedResults.entrySet()) { List groupedIndexes = entry.getValue(); groupedIndexes.sort(new Comparator() { final int highScore = 2; final int lowScore = 1; - @Override public int compare(PropertyIndex o1, PropertyIndex o2) { int o1Score = 0; @@ -190,8 +187,7 @@ public int compare(PropertyIndex o1, PropertyIndex o2) { return PageUtil.page(all, pageNo, pageSize); } - private PropertyIndex get(String name, int connId) { - HugeClient client = this.client(connId); + private PropertyIndex get(String name, HugeClient client) { try { IndexLabel indexLabel = client.schema().getIndexLabel(name); return convert(indexLabel); @@ -219,14 +215,14 @@ public List removeBatch(List indexLabels, HugeClient client) { } public void checkConflict(List entities, - ConflictDetail detail, int connId, + ConflictDetail detail, HugeClient client, boolean compareEachOther) { if (CollectionUtils.isEmpty(entities)) { return; } Map originEntities = new HashMap<>(); - for (PropertyIndex entity : this.list(connId)) { + for (PropertyIndex entity : this.list(client)) { originEntities.put(entity.getName(), entity); } for (PropertyIndex entity : entities) { @@ -244,11 +240,10 @@ public void checkConflict(List entities, } } - public ConflictStatus checkConflict(PropertyIndex entity, int connId) { - HugeClient client = this.client(connId); + public ConflictStatus checkConflict(PropertyIndex entity, HugeClient client) { String name = entity.getName(); IndexLabel newIndexLabel = convert(entity, client); - IndexLabel oldIndexLabel = convert(this.get(name, connId), client); + IndexLabel oldIndexLabel = convert(this.get(name, client), client); if (oldIndexLabel == null) { return ConflictStatus.PASSED; } else if (isEqual(newIndexLabel, oldIndexLabel)) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyKeyService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyKeyService.java index db5757d6c..999de5fec 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyKeyService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/PropertyKeyService.java @@ -27,6 +27,9 @@ import java.util.function.BiConsumer; import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.schema.ConflictCheckEntity; @@ -42,8 +45,6 @@ import org.apache.hugegraph.structure.schema.PropertyKey; import org.apache.hugegraph.structure.schema.VertexLabel; import org.apache.hugegraph.util.Ex; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import lombok.extern.log4j.Log4j2; @@ -51,17 +52,18 @@ @Service public class PropertyKeyService extends SchemaService { - public List list(int connId) { - return this.list(Collections.emptyList(), connId); + public List list(HugeClient client) { + return this.list(Collections.emptyList(), client); } - public List list(Collection names, int connId) { - return this.list(names, connId, true); + public List list(Collection names, + HugeClient client) { + return this.list(names, client, true); } - public List list(Collection names, int connId, + public List list(Collection names, + HugeClient client, boolean emptyAsAll) { - HugeClient client = this.client(connId); List propertyKeys; if (CollectionUtils.isEmpty(names)) { if (emptyAsAll) { @@ -79,8 +81,7 @@ public List list(Collection names, int connId, return results; } - public PropertyKeyEntity get(String name, int connId) { - HugeClient client = this.client(connId); + public PropertyKeyEntity get(String name, HugeClient client) { try { PropertyKey propertyKey = client.schema().getPropertyKey(name); return convert(propertyKey); @@ -94,14 +95,27 @@ public PropertyKeyEntity get(String name, int connId) { } } - public void checkExist(String name, int connId) { + public List get(List pks, HugeClient client) { + try { + return client.schema().getPropertyKeys(pks); + } catch (ServerException e) { + if (e.status() == Constant.STATUS_NOT_FOUND) { + throw new ExternalException("schema.propertykey.not-exist", + e, pks); + } + throw new ExternalException("schema.propertykey.get.failed", + e, pks); + } + } + + public void checkExist(String name, HugeClient client) { // Throw exception if it doesn't exist - this.get(name, connId); + this.get(name, client); } - public void checkNotExist(String name, int connId) { + public void checkNotExist(String name, HugeClient client) { try { - this.get(name, connId); + this.get(name, client); } catch (ExternalException e) { Throwable cause = e.getCause(); if (cause instanceof ServerException && @@ -113,14 +127,12 @@ public void checkNotExist(String name, int connId) { throw new ExternalException("schema.propertykey.exist", name); } - public void add(PropertyKeyEntity entity, int connId) { - HugeClient client = this.client(connId); + public void add(PropertyKeyEntity entity, HugeClient client) { PropertyKey propertyKey = convert(entity, client); client.schema().addPropertyKey(propertyKey); } - public void remove(String name, int connId) { - HugeClient client = this.client(connId); + public void remove(String name, HugeClient client) { client.schema().removePropertyKey(name); } @@ -128,8 +140,7 @@ public void remove(String name, int connId) { * Check the property key is being used, used means that there is * any vertex label or edge label contains the property(name) */ - public boolean checkUsing(String name, int connId) { - HugeClient client = this.client(connId); + public boolean checkUsing(String name, HugeClient client) { List vertexLabels = client.schema().getVertexLabels(); for (VertexLabel vertexLabel : vertexLabels) { if (vertexLabel.properties().contains(name)) { @@ -146,26 +157,27 @@ public boolean checkUsing(String name, int connId) { } public ConflictDetail checkConflict(ConflictCheckEntity entity, - int connId, boolean compareEachOther) { + HugeClient client, + boolean compareEachOther) { ConflictDetail detail = new ConflictDetail(SchemaType.PROPERTY_KEY); if (CollectionUtils.isEmpty(entity.getPkEntities())) { return detail; } this.checkConflict(entity.getPkEntities(), detail, - connId, compareEachOther); + client, compareEachOther); return detail; } public void checkConflict(List entities, - ConflictDetail detail, int connId, + ConflictDetail detail, HugeClient client, boolean compareEachOther) { if (CollectionUtils.isEmpty(entities)) { return; } Map originEntities = new HashMap<>(); - for (PropertyKeyEntity entity : this.list(connId)) { + for (PropertyKeyEntity entity : this.list(client)) { originEntities.put(entity.getName(), entity); } // Compare reused entity with origin in target graph @@ -180,10 +192,9 @@ public void checkConflict(List entities, } } - public void reuse(ConflictDetail detail, int connId) { + public void reuse(ConflictDetail detail, HugeClient client) { // Assume that the conflict detail is valid Ex.check(!detail.hasConflict(), "schema.cannot-reuse-conflict"); - HugeClient client = this.client(connId); List propertyKeys = this.filter(detail, client); if (propertyKeys.isEmpty()) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java index 5494625eb..e76c27aa0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java @@ -18,38 +18,47 @@ package org.apache.hugegraph.service.schema; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.stream.Collectors; - +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.driver.SchemaManager; import org.apache.hugegraph.entity.schema.ConflictDetail; import org.apache.hugegraph.entity.schema.ConflictStatus; +import org.apache.hugegraph.entity.schema.EdgeLabelEntity; import org.apache.hugegraph.entity.schema.Property; import org.apache.hugegraph.entity.schema.PropertyIndex; +import org.apache.hugegraph.entity.schema.PropertyKeyEntity; import org.apache.hugegraph.entity.schema.SchemaConflict; import org.apache.hugegraph.entity.schema.SchemaEntity; import org.apache.hugegraph.entity.schema.SchemaLabelEntity; import org.apache.hugegraph.entity.schema.SchemaType; -import org.apache.hugegraph.service.HugeClientPoolService; +import org.apache.hugegraph.entity.schema.VertexLabelEntity; +import org.apache.hugegraph.exception.InternalException; import org.apache.hugegraph.structure.SchemaElement; +import org.apache.hugegraph.structure.constant.IdStrategy; import org.apache.hugegraph.structure.schema.IndexLabel; +import org.apache.hugegraph.structure.schema.PropertyKey; import org.apache.hugegraph.structure.schema.SchemaLabel; import org.apache.hugegraph.util.HubbleUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; -import lombok.extern.log4j.Log4j2; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.stream.Collectors; @Log4j2 @Service @@ -61,35 +70,120 @@ public class SchemaService { @Autowired private HugeConfig config; @Autowired - private HugeClientPoolService poolService; + private PropertyKeyService pkService; + @Autowired + private VertexLabelService vlService; + @Autowired + private EdgeLabelService elService; public HugeConfig config() { return this.config; } - public HugeClient client(int connId) { - return this.poolService.getOrCreate(connId); - } - public static List collectNames( - List schemas) { + List schemas) { return schemas.stream().map(SchemaElement::name) .collect(Collectors.toList()); } - public static Set collectProperties(SchemaLabel schemaLabel) { + public SchemaView getSchemaView(HugeClient client) { + List propertyKeys = this.pkService.list(client); + List vertexLabels = this.vlService.list(client); + List edgeLabels = this.elService.list(client); + + List> vertices = new ArrayList<>(vertexLabels.size()); + for (VertexLabelEntity entity : vertexLabels) { + Map vertex = new LinkedHashMap<>(); + vertex.put("id", entity.getName()); + vertex.put("label", entity.getName()); + if (entity.getIdStrategy() == IdStrategy.PRIMARY_KEY) { + vertex.put("primary_keys", entity.getPrimaryKeys()); + } else { + vertex.put("primary_keys", new ArrayList<>()); + } + Map properties = new LinkedHashMap<>(); + this.fillProperties(properties, entity, propertyKeys); + vertex.put("properties", properties); + vertex.put("~style", entity.getStyle()); + vertices.add(vertex); + } + + List> edges = new ArrayList<>(edgeLabels.size()); + for (EdgeLabelEntity entity : edgeLabels) { + Map edge = new LinkedHashMap<>(); + String edgeId = String.format( + "%s-%s->%s", entity.getSourceLabel(), + entity.getName(), entity.getTargetLabel()); + edge.put("id", edgeId); + edge.put("label", entity.getName()); + edge.put("source", entity.getSourceLabel()); + edge.put("target", entity.getTargetLabel()); + if (entity.isLinkMultiTimes()) { + edge.put("sort_keys", entity.getSortKeys()); + } else { + edge.put("sort_keys", new ArrayList<>()); + } + Map properties = new LinkedHashMap<>(); + this.fillProperties(properties, entity, propertyKeys); + edge.put("properties", properties); + edge.put("~style", entity.getStyle()); + edges.add(edge); + } + return new SchemaView(vertices, edges); + } + + @Data + @AllArgsConstructor + public static class SchemaView { + + @JsonProperty("vertices") + private List> vertices; + + @JsonProperty("edges") + private List> edges; + } + + private void fillProperties(Map properties, + SchemaLabelEntity entity, + List propertyKeys) { + for (Property property : entity.getProperties()) { + String name = property.getName(); + PropertyKeyEntity pkEntity = findPropertyKey(propertyKeys, name); + properties.put(name, pkEntity.getDataType().string()); + } + } + + private PropertyKeyEntity findPropertyKey(List entities, + String name) { + for (PropertyKeyEntity entity : entities) { + if (entity.getName().equals(name)) { + return entity; + } + } + throw new InternalException("schema.propertykey.not-exist", name); + } + + public Set collectProperties(SchemaLabel schemaLabel, + HugeClient client) { Set properties = new HashSet<>(); Set nullableKeys = schemaLabel.nullableKeys(); - for (String property : schemaLabel.properties()) { - boolean nullable = nullableKeys.contains(property); - properties.add(new Property(property, nullable)); + List pkNames = + schemaLabel.properties().stream().collect(Collectors.toList()); + if (pkNames.size() == 0) { + return properties; + } + List propertyKeys = this.pkService.get(pkNames, client); + for (PropertyKey pk : propertyKeys) { + boolean nullable = nullableKeys.contains(pk.name()); + properties.add(new Property(pk.name(), nullable, pk.dataType(), + pk.cardinality())); } return properties; } public static List collectPropertyIndexes( - SchemaLabel schemaLabel, - List indexLabels) { + SchemaLabel schemaLabel, + List indexLabels) { List propertyIndexes = new ArrayList<>(); if (indexLabels == null) { return propertyIndexes; @@ -164,7 +258,7 @@ public static List convertIndexLabels(List entities, } public static - void compareWithEachOther(ConflictDetail detail, SchemaType type) { + void compareWithEachOther(ConflictDetail detail, SchemaType type) { List> conflicts = detail.getConflicts(type); for (int i = 0; i < conflicts.size(); i++) { SchemaConflict conflict = conflicts.get(i); @@ -177,8 +271,8 @@ void compareWithEachOther(ConflictDetail detail, SchemaType type) { } public static - ConflictStatus compareWithOthers(int currentIdx, - List> conflicts) { + ConflictStatus compareWithOthers(int currentIdx, + List> conflicts) { SchemaConflict current = conflicts.get(currentIdx); T currentEntity = current.getEntity(); // May changed @@ -204,8 +298,8 @@ ConflictStatus compareWithOthers(int currentIdx, } public static void addBatch( - List schemas, HugeClient client, - BiConsumer func, SchemaType type) { + List schemas, HugeClient client, + BiConsumer func, SchemaType type) { if (CollectionUtils.isEmpty(schemas)) { return; } @@ -220,8 +314,8 @@ public static void addBatch( } public static List addBatch( - List schemas, HugeClient client, - BiFunction func, SchemaType type) { + List schemas, HugeClient client, + BiFunction func, SchemaType type) { List tasks = new ArrayList<>(); if (CollectionUtils.isEmpty(schemas)) { return tasks; @@ -238,8 +332,8 @@ public static List addBatch( } public static List removeBatch( - List names, HugeClient client, - BiFunction func, SchemaType type) { + List names, HugeClient client, + BiFunction func, SchemaType type) { List tasks = new ArrayList<>(); if (CollectionUtils.isEmpty(names)) { return tasks; @@ -264,7 +358,8 @@ public static void removeBatch(List names, HugeClient client, public static Date getCreateTime(SchemaElement element) { Object createTimeValue = element.userdata().get(USER_KEY_CREATE_TIME); if (!(createTimeValue instanceof Long)) { - return new Date(0); + // return new Date(0L); + return HubbleUtil.nowDate(); } return new Date((long) createTimeValue); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/VertexLabelService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/VertexLabelService.java index 0ab420bb5..af79d91db 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/VertexLabelService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/VertexLabelService.java @@ -30,6 +30,10 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.schema.ConflictCheckEntity; @@ -52,9 +56,6 @@ import org.apache.hugegraph.structure.schema.VertexLabel; import org.apache.hugegraph.util.Ex; import org.apache.hugegraph.util.JsonUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import lombok.extern.log4j.Log4j2; @@ -67,17 +68,18 @@ public class VertexLabelService extends SchemaService { @Autowired private PropertyIndexService piService; - public List list(int connId) { - return this.list(Collections.emptyList(), connId); + public List list(HugeClient client) { + return this.list(Collections.emptyList(), client); } - public List list(Collection names, int connId) { - return this.list(names, connId, true); + public List list(Collection names, + HugeClient client) { + return this.list(names, client, true); } - public List list(Collection names, int connId, + public List list(Collection names, + HugeClient client, boolean emptyAsAll) { - HugeClient client = this.client(connId); List vertexLabels; if (CollectionUtils.isEmpty(names)) { if (emptyAsAll) { @@ -92,17 +94,16 @@ public List list(Collection names, int connId, List results = new ArrayList<>(vertexLabels.size()); vertexLabels.forEach(vertexLabel -> { - results.add(join(vertexLabel, indexLabels)); + results.add(join(vertexLabel, indexLabels, client)); }); return results; } - public VertexLabelEntity get(String name, int connId) { - HugeClient client = this.client(connId); + public VertexLabelEntity get(String name, HugeClient client) { try { VertexLabel vertexLabel = client.schema().getVertexLabel(name); List indexLabels = client.schema().getIndexLabels(); - return join(vertexLabel, indexLabels); + return join(vertexLabel, indexLabels, client); } catch (ServerException e) { if (e.status() == Constant.STATUS_NOT_FOUND) { throw new ExternalException("schema.vertexlabel.not-exist", @@ -113,14 +114,14 @@ public VertexLabelEntity get(String name, int connId) { } } - public void checkExist(String name, int connId) { + public void checkExist(String name, HugeClient client) { // Throw exception if it doesn't exist - this.get(name, connId); + this.get(name, client); } - public void checkNotExist(String name, int connId) { + public void checkNotExist(String name, HugeClient client) { try { - this.get(name, connId); + this.get(name, client); } catch (ExternalException e) { Throwable cause = e.getCause(); if (cause instanceof ServerException && @@ -132,8 +133,7 @@ public void checkNotExist(String name, int connId) { throw new ExternalException("schema.vertexlabel.exist", name); } - public List getLinkEdgeLabels(String name, int connId) { - HugeClient client = this.client(connId); + public List getLinkEdgeLabels(String name, HugeClient client) { List edgeLabels = client.schema().getEdgeLabels(); List results = new ArrayList<>(); for (EdgeLabel edgeLabel : edgeLabels) { @@ -144,8 +144,7 @@ public List getLinkEdgeLabels(String name, int connId) { return results; } - public void add(VertexLabelEntity entity, int connId) { - HugeClient client = this.client(connId); + public void add(VertexLabelEntity entity, HugeClient client) { VertexLabel vertexLabel = convert(entity, client); try { client.schema().addVertexLabel(vertexLabel); @@ -153,22 +152,26 @@ public void add(VertexLabelEntity entity, int connId) { throw new ExternalException("schema.vertexlabel.create.failed", e, entity.getName()); } - List indexLabels = collectIndexLabels(entity, client); - this.piService.addBatch(indexLabels, client); + try { + List indexLabels = collectIndexLabels(entity, client); + this.piService.addBatch(indexLabels, client); + } catch (Exception e) { + client.schema().removeVertexLabel(vertexLabel.name()); + throw new ExternalException("schema.vertexlabel.create.failed", + e, entity.getName()); + } } - public void update(VertexLabelUpdateEntity entity, int connId) { - HugeClient client = this.client(connId); + public void update(VertexLabelUpdateEntity entity, HugeClient client) { VertexLabel vertexLabel = convert(entity, client); - // All existed indexlabels List existedIndexLabels = client.schema().getIndexLabels(); List existedIndexLabelNames = collectNames(existedIndexLabels); List addedIndexLabelNames = entity.getAppendPropertyIndexNames(); List addedIndexLabels = convertIndexLabels( - entity.getAppendPropertyIndexes(), - client, true, entity.getName()); + entity.getAppendPropertyIndexes(), + client, true, entity.getName()); List removedIndexLabelNames = entity.getRemovePropertyIndexes(); @@ -176,8 +179,8 @@ public void update(VertexLabelUpdateEntity entity, int connId) { for (String name : addedIndexLabelNames) { if (existedIndexLabelNames.contains(name)) { throw new ExternalException( - "schema.vertexlabel.update.append-index-existed", - entity.getName(), name); + "schema.vertexlabel.update.append-index-existed", + entity.getName(), name); } } } @@ -185,8 +188,8 @@ public void update(VertexLabelUpdateEntity entity, int connId) { for (String name : removedIndexLabelNames) { if (!existedIndexLabelNames.contains(name)) { throw new ExternalException( - "schema.vertexlabel.update.remove-index-unexisted", - entity.getName(), name); + "schema.vertexlabel.update.remove-index-unexisted", + entity.getName(), name); } } } @@ -202,13 +205,11 @@ public void update(VertexLabelUpdateEntity entity, int connId) { this.piService.removeBatch(removedIndexLabelNames, client); } - public void remove(String name, int connId) { - HugeClient client = this.client(connId); + public void remove(String name, HugeClient client) { client.schema().removeVertexLabelAsync(name); } - public boolean checkUsing(String name, int connId) { - HugeClient client = this.client(connId); + public boolean checkUsing(String name, HugeClient client) { List edgeLabels = client.schema().getEdgeLabels(); for (EdgeLabel edgeLabel : edgeLabels) { if (edgeLabel.linkedVertexLabel(name)) { @@ -219,30 +220,31 @@ public boolean checkUsing(String name, int connId) { } public ConflictDetail checkConflict(ConflictCheckEntity entity, - int connId, boolean compareEachOther) { + HugeClient client, + boolean compareEachOther) { ConflictDetail detail = new ConflictDetail(SchemaType.VERTEX_LABEL); if (CollectionUtils.isEmpty(entity.getVlEntities())) { return detail; } this.pkService.checkConflict(entity.getPkEntities(), detail, - connId, compareEachOther); + client, compareEachOther); this.piService.checkConflict(entity.getPiEntities(), detail, - connId, compareEachOther); + client, compareEachOther); this.checkConflict(entity.getVlEntities(), detail, - connId, compareEachOther); + client, compareEachOther); return detail; } public void checkConflict(List entities, - ConflictDetail detail, int connId, + ConflictDetail detail, HugeClient client, boolean compareEachOther) { if (CollectionUtils.isEmpty(entities)) { return; } Map originEntities = new HashMap<>(); - for (VertexLabelEntity entity : this.list(connId)) { + for (VertexLabelEntity entity : this.list(client)) { originEntities.put(entity.getName(), entity); } for (VertexLabelEntity entity : entities) { @@ -264,10 +266,9 @@ public void checkConflict(List entities, } } - public void reuse(ConflictDetail detail, int connId) { + public void reuse(ConflictDetail detail, HugeClient client) { // Assume that the conflict detail is valid Ex.check(!detail.hasConflict(), "schema.cannot-reuse-conflict"); - HugeClient client = this.client(connId); List propertyKeys = this.pkService.filter(detail, client); if (!propertyKeys.isEmpty()) { @@ -324,14 +325,16 @@ public void removeBatch(List vertexLabels, HugeClient client) { removeBatch(names, client, func, SchemaType.VERTEX_LABEL); } - private static VertexLabelEntity join(VertexLabel vertexLabel, - List indexLabels) { + private VertexLabelEntity join(VertexLabel vertexLabel, + List indexLabels, + HugeClient client) { if (vertexLabel == null) { return null; } - Set properties = collectProperties(vertexLabel); - List propertyIndexes = collectPropertyIndexes(vertexLabel, - indexLabels); + Set properties = collectProperties(vertexLabel, client); + List propertyIndexes = + collectPropertyIndexes(vertexLabel, + indexLabels); return VertexLabelEntity.builder() .name(vertexLabel.name()) .properties(properties) @@ -365,7 +368,7 @@ private static VertexLabel convert(VertexLabelEntity entity, .properties(toStringArray(entity.getPropNames())) .primaryKeys(toStringArray(entity.getPrimaryKeys())) .nullableKeys(toStringArray(entity.getNullableProps())) - .enableLabelIndex(entity.isOpenLabelIndex()) + .enableLabelIndex(entity.getOpenLabelIndex()) .userdata(USER_KEY_CREATE_TIME, entity.getCreateTime()) .userdata(USER_KEY_STYLE, JsonUtil.toJson(style)) .build(); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/ComputerService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/ComputerService.java new file mode 100644 index 000000000..73a33ea79 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/ComputerService.java @@ -0,0 +1,94 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.space; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.hugegraph.rest.SerializeException; +import org.apache.hugegraph.util.JsonUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.space.ComputerServiceEntity; +import org.apache.hugegraph.structure.Task; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.PageUtil; + +@Log4j2 +@Service +public class ComputerService { + public IPage queryPage(HugeClient client, + String query, + int pageNo, + int pageSize) { + List results = new ArrayList<>(); + List tasks = client.computer().list(500); + tasks.stream().skip(Math.max(pageNo - 1, 0) * pageSize).limit(pageSize) + .forEach((t) -> { + ComputerServiceEntity entity = get(client, t.id()); + results.add(entity); + }); + + int count = tasks.size(); + + return PageUtil.newPage(results, pageNo, pageSize, count); + } + + public ComputerServiceEntity get(HugeClient client, long id) { + return convert(client.computer().get(id)); + } + + public void cancel(HugeClient client, long id) { + client.computer().cancel(id); + } + + protected ComputerServiceEntity convert(Task task) { + ComputerServiceEntity entity = new ComputerServiceEntity(); + entity.setId(task.id()); + entity.setName(task.name()); + entity.setType(task.type()); + entity.setStatus(task.status()); + entity.setProgress(task.progress()); + entity.setDescription(task.description()); + entity.setCreate(task.createTime()); + + if (StringUtils.isNotEmpty(task.input())) { + try { + Map input = HubbleUtil.uncheckedCast( + JsonUtil.fromJson(task.input(), Map.class)); + + entity.setAlgorithm(input.get("algorithm").toString()); + entity.setInput(input.get("params").toString()); + } catch (SerializeException e) { + log.warn("Failed to parse computer task input: {}", + e.getMessage()); + } + } + return entity; + } + + public void delete(HugeClient client, long id) { + client.computer().delete(id); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java new file mode 100644 index 000000000..7428944a6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java @@ -0,0 +1,331 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.space; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.hugegraph.api.task.TasksWithPage; +// TODO fix import +//import org.apache.hugegraph.client.api.task.TasksWithPage; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.space.GraphSpaceEntity; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.service.graphs.GraphsService; +import org.apache.hugegraph.structure.Task; +import org.apache.hugegraph.structure.space.GraphSpace; +import org.apache.hugegraph.structure.space.SchemaTemplate; +import org.apache.hugegraph.util.GremlinUtil; +import org.apache.hugegraph.util.HubbleUtil; +import org.apache.hugegraph.util.Log; +import org.apache.hugegraph.util.PageUtil; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Comparator; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class GraphSpaceService { + + private static final Logger LOG = Log.logger(GraphSpaceService.class); + + @Autowired + private UserService userService; + + @Autowired + private GraphsService graphsService; + + /** + * 统计所有图空间总数、图总数 统计顶点总数和边总数、顶点类型及边类型总数、前一天执行任务个数 + * + * @param client hugegraph-client + * @return 统计结果集 + */ + public Map metrics(HugeClient client) { + long gsCount = 0L; + long gCount = 0L; + long vCount = 0L; + long eCount = 0L; + long vlCount = 0L; + long elCount = 0L; + long preDayTaskCount = 0L; + try { + List graphSpaces = this.listAll(client); + gsCount = graphSpaces.size(); + + for (String gs : graphSpaces) { + Map ev = evCount(client, gs); + Map elVl = elAndVlCount(client, gs); + Map task = preDayTaskCount(client, gs); + + vCount += (Long) ev.get("vertex"); + eCount += (Long) ev.get("edge"); + vlCount += (Long) elVl.get("vertexlabel"); + elCount += (Long) elVl.get("edgelabel"); + preDayTaskCount += (Long) task.get("task"); + + gCount += Long.valueOf( + graphsService.listGraphNames(client, gs, "").size()); + } + } catch (Exception e) { + LOG.error("Failed to get SaaS metrics", e); + throw e; + } + + HashMap res = new HashMap<>(); + res.put("gsCount", gsCount); + res.put("gCount", gCount); + res.put("vCount", vCount); + res.put("eCount", eCount); + res.put("vlCount", vlCount); + res.put("elCount", elCount); + res.put("preDayTaskCount", preDayTaskCount); + return res; + } + + public IPage> queryPage(HugeClient client, String query, + String createTime, int pageNo, + int pageSize) { + List> results = + queryAllGs(client, query, createTime); + return PageUtil.page(results, pageNo, pageSize); + } + + public List> queryAllGs(HugeClient client, String query, + String createTime) { + List> results = + client.graphSpace().listProfile(query).stream() + .filter((s) -> s.get("create_time").toString() + .compareTo(createTime) > 0) + .collect(Collectors.toList()); + // 将DEFAULT和neizhianli的图空间排在前面, 其他图空间按字母序排序 + Collections.sort(results, (a, b) -> + new BuiltInFirst().compare(a.get("name").toString(), + b.get("name").toString())); + for (Map info : results) { + String name = info.get("name").toString(); + info.put("graphspace_admin", + userService.listGraphSpaceAdmin(client, name)); + Map statisticTotal = evCount(client, name); + info.put("statistic", statisticTotal); + } + return results; + } + + /** + * 统计指定图空间下的顶点总数和边总数 + * @param client + * @param graphSpace + * @return + */ + private Map evCount(HugeClient client, String graphSpace) { + long vertexTotal = 0L; + long edgeTotal = 0L; + Map statisticTotal = new HashMap<>(); + client.assignGraph(graphSpace, ""); + Set graphs = graphsService.listGraphNames(client, graphSpace, ""); + String statisticDate = HubbleUtil.dateFormatDay(HubbleUtil.nowDate()); + for (String graph : graphs) { + Map graphEvCount = + graphsService.evCount(client, graphSpace, graph); + + vertexTotal += (long) graphEvCount.get("vertex"); + edgeTotal += (long) graphEvCount.get("edge"); + } + statisticTotal.put("date", HubbleUtil.dateFormatDay(statisticDate)); + statisticTotal.put("vertex", vertexTotal); + statisticTotal.put("edge", edgeTotal); + return statisticTotal; + } + + /** + * 统计指定图空间下的edgeLabel总数和vertexLabel边总数 + * @param client + * @param graphSpace + * @return + */ + private Map elAndVlCount(HugeClient client, + String graphSpace) { + Map result = new HashMap<>(); + Set graphs = + graphsService.listGraphNames(client, graphSpace, ""); + long vlCount = 0L; + long elCount = 0L; + for (String graph : graphs) { + client.assignGraph(graphSpace, graph); + vlCount += client.schema().getVertexLabels().size(); + elCount += client.schema().getEdgeLabels().size(); + } + + result.put("vertexlabel", vlCount); + result.put("edgelabel", elCount); + return result; + } + + /** + * 前一天的执行任务数 + * + * @param client + * @param graphSpace + * @return + */ + private Map preDayTaskCount(HugeClient client, + String graphSpace) { + LOG.info("Task count in [{}]", graphSpace); + String lastDay = HubbleUtil.dateFormatLastDay(); + Date start = null; + Date end = null; + try { + SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyyMMdd HH:mm:ss"); + start = simpleDateFormat.parse(lastDay + " 00:00:00"); + end = simpleDateFormat.parse(lastDay + " 23:59:59"); + } catch (ParseException e) { + throw new InternalException("parse date error" + e.getMessage()); + } + + Map result = new HashMap<>(); + Set graphs = + graphsService.listGraphNames(client, graphSpace, ""); + long preDayTaskCount = 0L; + for (String graph : graphs) { + client.assignGraph(graphSpace, graph); + String page = ""; + while (page != null) { + TasksWithPage pageTask = + client.task().list(null, page, 1000); + for (Task task : pageTask.tasks()) { + if (task.createTime() >= start.getTime() && + task.createTime() <= end.getTime()) { + preDayTaskCount += 1; + } + } + page = pageTask.page(); + } + } + result.put("task", preDayTaskCount); + return result; + } + + public boolean isAuth(HugeClient client, String graphSpace) { + GraphSpace space = getWithoutAdmins(client, graphSpace); + + return space.isAuth(); + } + + public List listAll(HugeClient client) { + List result = client.graphSpace().listGraphSpace().stream() + .collect(Collectors.toList()); + Collections.sort(result, (a, b) -> new BuiltInFirst().compare(a, b)); + return result; + } + + public GraphSpace getWithoutAdmins(HugeClient authClient, + String graphspace) { + GraphSpace space = authClient.graphSpace().getGraphSpace(graphspace); + if (space == null) { + throw new InternalException("graphspace.get.{} Not Exits", + graphspace); + } + + return space; + } + + public GraphSpaceEntity getWithAdmins(HugeClient authClient, String graphspace) { + GraphSpace space = authClient.graphSpace().getGraphSpace(graphspace); + if (space == null) { + throw new InternalException("graphspace.get.{} Not Exits", + graphspace); + } + + GraphSpaceEntity graphSpaceEntity + = GraphSpaceEntity.fromGraphSpace(space); + + if (authClient.auth().isSuperAdmin()) { + graphSpaceEntity.graphspaceAdmin = + userService.listGraphSpaceAdmin(authClient, graphspace); + } + graphSpaceEntity.setStatistic(evCount(authClient, graphspace)); + + return graphSpaceEntity; + } + + public void delete(HugeClient authClient, String graphspace) { + authClient.graphSpace() + .deleteGraphSpace(graphspace); + } + + public Object create(HugeClient authClient, GraphSpace graphSpace) { + return authClient.graphSpace().createGraphSpace(graphSpace); + } + + public GraphSpace update(HugeClient authClient, GraphSpace graphSpace) { + return authClient.graphSpace().updateGraphSpace(graphSpace); + } + + public void initBuiltIn(HugeClient client) { + String builtInStr = Constant.BUILT_IN; + GraphSpace builtIn = new GraphSpace(builtInStr); + builtIn.setNickname("内置案例"); + builtIn.setDescription("内置案例"); + builtIn.setMaxGraphNumber(100); + builtIn.setCpuLimit(1000); + builtIn.setStorageLimit(1000); + builtIn.setMemoryLimit(1024); + builtIn.setOlapNamespace("built_in"); + builtIn.setOltpNamespace("built_in"); + + List spaces = client.graphSpace().listGraphSpace(); + if (spaces.contains(builtInStr)) { + client.graphSpace().deleteGraphSpace(builtInStr); + } + client.graphSpace().createGraphSpace(builtIn); + + client.assignGraph(builtInStr, null); + SchemaTemplate schemaTemplate = new SchemaTemplate("hlm", + GremlinUtil.GREMLIN_HLM_SCHEMA); + client.schemaTemplateManager().createSchemaTemplate(schemaTemplate); + + schemaTemplate = new SchemaTemplate("covid19", + GremlinUtil.GREMLIN_COVID19_SCHEMA); + client.schemaTemplateManager().createSchemaTemplate(schemaTemplate); + } + + private class BuiltInFirst implements Comparator { + @Override + public int compare(String o1, String o2) { + if (Constant.BUILT_IN.equals(o2)) { + return 1; + } else if (Constant.BUILT_IN.equals(o1)) { + return -1; + } + return o1.compareTo(o2); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/HStoreService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/HStoreService.java new file mode 100644 index 000000000..5bee38319 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/HStoreService.java @@ -0,0 +1,45 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.space; + +import java.util.ArrayList; +import java.util.List; + +import com.baomidou.mybatisplus.core.metadata.IPage; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.structure.space.HStoreNodeInfo; +import org.apache.hugegraph.util.PageUtil; +import org.springframework.stereotype.Service; + +@Service +public class HStoreService { + public IPage listPage(HugeClient client, int pageNo, + int pageSize) { + List nodeNames = client.hStoreManager().list(); + List result = new ArrayList<>(nodeNames.size()); + nodeNames.stream().sorted().forEach(node -> { + HStoreNodeInfo nodeInfo = client.hStoreManager() + .get(node); + result.add(nodeInfo); + }); + + return PageUtil.page(result, pageNo, pageSize); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/OLTPServerService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/OLTPServerService.java new file mode 100644 index 000000000..d725ee9c6 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/OLTPServerService.java @@ -0,0 +1,113 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.space; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.factory.PDHugeClientFactory; +import org.apache.hugegraph.options.HubbleOptions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.core.metadata.IPage; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.util.PageUtil; +import org.apache.hugegraph.structure.space.OLTPService; +import org.springframework.util.CollectionUtils; + +@Service +public class OLTPServerService { + + @Autowired + protected String cluster; + @Autowired(required = false) + PDHugeClientFactory pdHugeClientFactory; + @Autowired + private HugeConfig config; + + public IPage queryPage(HugeClient client, String query, + int pageNo, int pageSize) { + List serviceNames = client.serviceManager().listService(); + List result = serviceNames.stream().filter(s -> s.contains(query)).sorted() + .map((s) -> get(client, s)) + .collect(Collectors.toList()); + + return PageUtil.page(result, pageNo, pageSize); + } + + public Object get(HugeClient client, String serviceName) { + // 获取当前所属图空间 + String graphSpace = client.getGraphSpaceName(); + OLTPService service = client.serviceManager().getService(serviceName); + + // 通过PD, 获取当前service可用URL, 判断当前服务是否存活 + List urls; + boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED); + if (pdEnabled && pdHugeClientFactory != null) { + urls = pdHugeClientFactory.getURLs(cluster, graphSpace, serviceName); + urls = urls.stream().distinct().collect(Collectors.toList()); + } else { + urls = Collections.emptyList(); + } + + // service使用pd中的urls + service.setUrls(urls); + + if (!service.checkIsK8s()) { + // manual service: 通过PD判断当前服务状态 + // 设置当前运行节点数 + service.setRunning((int) urls.size()); + + if (!CollectionUtils.isEmpty(urls)) { + service.setStatus(OLTPService.ServiceStatus.RUNNING); + } else { + service.setStatus(OLTPService.ServiceStatus.STOPPED); + } + } + + return service; + } + + public Object create(HugeClient client, OLTPService service) { + return client.serviceManager().addService(service); + } + + public void delete(HugeClient client, String service) { + client.serviceManager().delService(service, "I'm sure to delete the service"); + } + + public Object update(HugeClient client, OLTPService service) { + return client.serviceManager().updateService(service); + } + + public void start(HugeClient client, String service) { + client.serviceManager().startService(service); + } + + public void stop(HugeClient client, String service) { + client.serviceManager().stopService(service); + } + + public List configOptionList(HugeClient client) { + return client.serviceManager().configOptinList(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/SchemaTemplateService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/SchemaTemplateService.java new file mode 100644 index 000000000..5d18d09f3 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/SchemaTemplateService.java @@ -0,0 +1,68 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.space; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springframework.stereotype.Service; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.structure.space.SchemaTemplate; +import org.apache.hugegraph.util.PageUtil; + +@Service +public class SchemaTemplateService { + + public List listName(HugeClient client) { + return client.schemaTemplateManager().listSchemTemplate(); + } + + public IPage queryPage(HugeClient client, String query, int pageNo, + int pageSize) { + List names = client.schemaTemplateManager().listSchemTemplate(); + + List results = + names.stream().filter((s) -> s.contains(query)).sorted() + .map((s) -> client.schemaTemplateManager() + .getSchemaTemplate(s) + ).collect(Collectors.toList()); + + return PageUtil.page(results, pageNo, pageSize); + } + + public Map get(HugeClient client, String name) { + return client.schemaTemplateManager().getSchemaTemplate(name); + } + + public Map create(HugeClient client, SchemaTemplate schemaTemplate) { + return client.schemaTemplateManager().createSchemaTemplate(schemaTemplate); + } + + public void delete(HugeClient client, String name) { + client.schemaTemplateManager().deleteSchemaTemplate(name); + } + + public Map update(HugeClient client, + SchemaTemplate schemaTemplate) { + return client.schemaTemplateManager().updateSchemaTemplate(schemaTemplate); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/VermeerService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/VermeerService.java new file mode 100644 index 000000000..0c34efec3 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/VermeerService.java @@ -0,0 +1,126 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.service.space; + +import com.google.common.collect.ImmutableMap; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.client.RestClient; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.HugeClientBuilder; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.HubbleUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Log4j2 +@Service +public class VermeerService { + @Autowired + private HugeConfig config; + + private static final String GET_SYS_CFG = "api/v1.0/memt_clu/config/getsyscfg"; + + public boolean isVermeerEnabled(String username, String password) { + Map vermeer = this.getVermeer(username, password, + false); + if (vermeer != null) { + return (Boolean) vermeer.get("enable"); + } + return false; + } + + public Map getVermeer(String username, String password, + boolean throwIfNoConfig) { + String dashboard = config.get(HubbleOptions.DASHBOARD_ADDRESS); + String protocol = config.get(HubbleOptions.SERVER_PROTOCOL); + if (throwIfNoConfig) { + E.checkArgument(StringUtils.isNotEmpty(dashboard), + "Please set 'dashboard.address' in config file " + + "conf/hugegraph-hubble.properties."); + } else if (StringUtils.isEmpty(dashboard)) { + return ImmutableMap.of("enable", false); + } + HugeClientBuilder builder = HugeClient.builder(protocol + "://" + dashboard, + null, + null, + true) + .configUser(username, password); + RestClient client = new RestClient(builder.url(), builder.token(), + builder.timeout(), + builder.maxConns(), + builder.maxConnsPerRoute(), + builder.trustStoreFile(), + builder.trustStorePassword()); + Map sertMap = ImmutableMap.of("sertype", "vermeer"); + boolean enable = false; + try { + Map result = HubbleUtil.uncheckedCast( + client.post(GET_SYS_CFG, sertMap).readObject(Map.class)); + Map data = + HubbleUtil.uncheckedCast(result.get("data")); + if (data == null || data.isEmpty()) { + enable = false; + } else { + enable = "true".equals(data.get("cfgvalue")); + } + } catch (Exception e) { + log.warn("Dashboard Vermeer configuration is unavailable; " + + "Vermeer integration is disabled for this request"); + enable = false; + } finally { + client.close(); + } + return ImmutableMap.of("enable", enable); + } + + public Map load(HugeClient client, + String graphspace, String graph, String taskType, + Map params) { + Map jsonTask = new HashMap<>(); + jsonTask.put("task_type", taskType); + jsonTask.put("graph", convert2VG(graphspace, graph)); + jsonTask.put("params", params); + return client.vermeer().post(jsonTask); + } + + public Map compute(HugeClient client, + String graphspace, String graph, + Map params) { + Map jsonTask = new HashMap<>(); + jsonTask.put("task_type", "compute"); + jsonTask.put("graph", convert2VG(graphspace, graph)); + jsonTask.put("params", params); + return client.vermeer().post(jsonTask); + } + + public void deleteGraph(HugeClient client, String graphspace, + String graph) { + client.vermeer().deleteGraphByName(convert2VG(graphspace, graph)); + } + + public String convert2VG(String space, String graph) { + return space + "-" + graph; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/ESUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/ESUtil.java new file mode 100644 index 000000000..0cdb55276 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/ESUtil.java @@ -0,0 +1,60 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.util; + +import org.apache.commons.lang3.StringUtils; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Map; + +public class ESUtil { + public static Object getValueByPath(Map map, + String[] keys) { + if (keys.length == 1) { + return map.getOrDefault(keys[0], null); + } else { + Object data1 = map.get(keys[0]); + if (data1 instanceof Map) { + return getValueByPath(HubbleUtil.uncheckedCast(data1), + Arrays.copyOfRange(keys, 1, + keys.length)); + } + + return data1; + } + } + + public static String parseTimestamp(String esTimestatmp) { + // 转换ES中的@timestamp为当前时区时间戳 + if (StringUtils.isEmpty(esTimestatmp)) { + return null; + } + ZonedDateTime dateTime = + ZonedDateTime.parse(esTimestatmp, + DateTimeFormatter.ISO_DATE_TIME); + dateTime = dateTime.withZoneSameInstant(ZoneId.systemDefault()); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd " + + "HH:mm:ss"); + return dateTime.format(dtf); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/EntityUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/EntityUtil.java index 68139a607..03906a5cf 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/EntityUtil.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/EntityUtil.java @@ -18,12 +18,12 @@ package org.apache.hugegraph.util; -import java.lang.reflect.Field; - import org.apache.hugegraph.annotation.MergeProperty; import org.apache.hugegraph.common.Mergeable; import org.apache.hugegraph.exception.InternalException; +import java.lang.reflect.Field; + public final class EntityUtil { @SuppressWarnings("unchecked") @@ -31,8 +31,8 @@ public static T merge(T oldEntity, T newEntity) { Class clazz = oldEntity.getClass(); T entity; try { - entity = (T) clazz.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { + entity = (T) clazz.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { throw new InternalException("reflect.new-instance.failed", e, clazz.getName()); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/Ex.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/Ex.java index e988a4f6d..8ea75869c 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/Ex.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/Ex.java @@ -18,11 +18,11 @@ package org.apache.hugegraph.util; -import java.util.concurrent.Callable; - import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.exception.InternalException; +import java.util.concurrent.Callable; + public final class Ex { public static void check(boolean expression, String message, diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/FileUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/FileUtil.java index 876b850a4..1157542ae 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/FileUtil.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/FileUtil.java @@ -38,7 +38,7 @@ public static int countLines(String path) { public static int countLines(File file) { if (!file.exists()) { throw new IllegalArgumentException(String.format( - "The file %s doesn't exist", file)); + "The file %s doesn't exist", file)); } long fileLength = file.length(); try (FileInputStream fis = new FileInputStream(file); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/GremlinUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/GremlinUtil.java index 352e1fc0c..55d21d837 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/GremlinUtil.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/GremlinUtil.java @@ -44,17 +44,17 @@ public final class GremlinUtil { ); private static final String[] COMPILE_SEARCH_LIST = new String[]{ - ".", "(", ")" + ".", "(", ")" }; private static final String[] COMPILE_TARGET_LIST = new String[]{ - "\\.", "\\(", "\\)" + "\\.", "\\(", "\\)" }; private static final String[] ESCAPE_SEARCH_LIST = new String[]{ - "\\", "\"", "'", "\n" + "\\", "\"", "'", "\r", "\n" }; private static final String[] ESCAPE_TARGET_LIST = new String[]{ - "\\\\", "\\\"", "\\'", "\\n" + "\\\\", "\\\"", "\\'", "\\r", "\\n" }; private static final Set LIMIT_PATTERNS = compile(LIMIT_SUFFIXES); @@ -63,24 +63,255 @@ public final class GremlinUtil { Pattern.compile("^\\s*//.*") ); + public static final String GREMLIN_HLM_SCHEMA = + "graph.schema().propertyKey('姓名').asText().ifNotExist().create();\n" + + "graph.schema().propertyKey('性别').asText().ifNotExist().create();\n" + + "graph.schema().propertyKey('年龄').asInt().ifNotExist().create();\n" + + "graph.schema().propertyKey('特点').asText().ifNotExist().create();\n" + + "graph.schema().propertyKey('亲疏').asText().ifNotExist().create();\n" + + "\n" + + "graph.schema().vertexLabel('男人')" + + ".properties('姓名', '性别', '年龄', '特点', '亲疏')" + + ".primaryKeys('姓名').ifNotExist().create();\n" + + "graph.schema().vertexLabel('女人')" + + ".properties('姓名', '性别', '年龄', '特点', '亲疏')" + + ".primaryKeys('姓名').ifNotExist().create();\n" + + "graph.schema().vertexLabel('机构')" + + ".properties('姓名', '特点', '亲疏')" + + ".primaryKeys('姓名').ifNotExist().create();\n" + + "\n" + + "graph.schema().edgeLabel('父子').sourceLabel('男人')" + + ".targetLabel('男人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('父女').sourceLabel('男人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('母子').sourceLabel('女人')" + + ".targetLabel('男人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('母女').sourceLabel('女人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('妻').sourceLabel('男人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('妾').sourceLabel('男人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('相恋').sourceLabel('男人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('朋友').sourceLabel('男人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('姐妹').sourceLabel('女人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('丫环').sourceLabel('男人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('丫头').sourceLabel('女人')" + + ".targetLabel('女人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('兵部指挥').sourceLabel('机构')" + + ".targetLabel('男人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('巡盐御史').sourceLabel('机构')" + + ".targetLabel('男人').ifNotExist().create();\n" + + "graph.schema().edgeLabel('尚书令').sourceLabel('机构')" + + ".targetLabel('男人').ifNotExist().create();\n\n\n"; + public static final String GREMLIN_HLM_DATA = + "jiataigong = graph.addVertex(T.label, '男人', '姓名', '贾太公', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l5');\n" + + "jiayan = graph.addVertex(T.label, '男人', '姓名', '贾演', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l6');\n" + + "jiadaihua = graph.addVertex(T.label, '男人', '姓名', '贾代化', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l5');\n" + + "jiayuan = graph.addVertex(T.label, '男人', '姓名', '贾源', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l4');\n" + + "jiadaishan = graph.addVertex(T.label, '男人', '姓名', '贾代善', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l3');\n" + + "jiamu = graph.addVertex(T.label, '女人', '姓名', '贾母', " + + "'性别', '女', '年龄', 0, '特点', '史太君,老祖宗', '亲疏', 'o-l3');\n" + + "jiajing = graph.addVertex(T.label, '男人', '姓名', '贾敬', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l5');\n" + + "jiazhen = graph.addVertex(T.label, '男人', '姓名', '贾珍', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'm-l4');\n" + + "youdajie = graph.addVertex(T.label, '女人', '姓名', '尤氏', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l4');\n" + + "jiarong = graph.addVertex(T.label, '男人', '姓名', '贾蓉', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "qingkeqing = graph.addVertex(T.label, '女人', '姓名', '秦可卿', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "jiashe = graph.addVertex(T.label, '男人', '姓名', '贾赦', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l4');\n" + + "jialian = graph.addVertex(T.label, '男人', '姓名', '贾琏', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'm-l3');\n" + + "wangxifeng = graph.addVertex(T.label, '女人', '姓名', '王熙凤', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l2');\n" + + "youerjie = graph.addVertex(T.label, '女人', '姓名', '尤二姐', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l3');\n" + + "pinger = graph.addVertex(T.label, '女人', '姓名', '平儿', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "qiutong = graph.addVertex(T.label, '女人', '姓名', '秋桐', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "jiayingchun = graph.addVertex(T.label, '女人', '姓名', '贾迎春', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "sunshaozu = graph.addVertex(T.label, '男人', '姓名', '孙绍祖', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "jiazheng = graph.addVertex(T.label, '男人', '姓名', '贾政', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'm-l2');\n" + + "wangfuren = graph.addVertex(T.label, '女人', '姓名', '王夫人', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l2');\n" + + "zhaoyiniang = graph.addVertex(T.label, '女人', '姓名', '赵姨娘', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l4');\n" + + "jiahuan = graph.addVertex(T.label, '男人', '姓名', '贾环', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'y-l4');\n" + + "jiatanchun = graph.addVertex(T.label, '女人', '姓名', '贾探春', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l4');\n" + + "jiayuanchun = graph.addVertex(T.label, '女人', '姓名', '贾元春', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "jiazhu = graph.addVertex(T.label, '男人', '姓名', '贾珠', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "liwan = graph.addVertex(T.label, '女人', '姓名', '李纨', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l3');\n" + + "jialan = graph.addVertex(T.label, '男人', '姓名', '贾兰', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'b-l4');\n" + + "jiabaoyu = graph.addVertex(T.label, '男人', '姓名', '贾宝玉', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'y-l1');\n" + + "xuebaochai = graph.addVertex(T.label, '女人', '姓名', '薛宝钗', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l2');\n" + + "jiamin = graph.addVertex(T.label, '女人', '姓名', '贾敏', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l3');\n" + + "linruhai = graph.addVertex(T.label, '男人', '姓名', '林如海', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'm-l3');\n" + + "lindaiyu = graph.addVertex(T.label, '女人', '姓名', '林黛玉', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l1');\n" + + "shihou = graph.addVertex(T.label, '男人', '姓名', '史候', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l6');\n" + + "shigong = graph.addVertex(T.label, '男人', '姓名', '史公', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l5');\n" + + "shiba = graph.addVertex(T.label, '男人', '姓名', '史氏', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'o-l4');\n" + + "shinai = graph.addVertex(T.label, '男人', '姓名', '史鼐', " + + "'性别', '男', '年龄', 0, '特点', '保龄侯', '亲疏', 'o-l4');\n" + + "shiding = graph.addVertex(T.label, '男人', '姓名', '史鼎', " + + "'性别', '男', '年龄', 0, '特点', '忠靖侯', '亲疏', 'o-l4');\n" + + "shixiangyun = graph.addVertex(T.label, '女人', '姓名', '史湘云', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'y-l3');\n" + + "weiruolan = graph.addVertex(T.label, '男人', '姓名', '卫若兰', " + + "'性别', '男', '年龄', 0, '特点', '-', '亲疏', 'm-l4');\n" + + "xueyima = graph.addVertex(T.label, '女人', '姓名', '薛姨妈', " + + "'性别', '女', '年龄', 0, '特点', '-', '亲疏', 'm-l3');\n" + + "\n" + + "jiataigong.addEdge('父子', jiayan);\n" + + "jiataigong.addEdge('父子', jiayuan);\n" + + "jiayan.addEdge('父子', jiadaihua);\n" + + "jiayuan.addEdge('父子', jiadaishan);\n" + + "jiadaishan.addEdge('妻', jiamu);\n" + + "jiadaihua.addEdge('父子', jiajing);\n" + + "jiajing.addEdge('父子', jiazhen);\n" + + "jiazhen.addEdge('父子', jiarong);\n" + + "jiazhen.addEdge('妾', youdajie);\n" + + "jiarong.addEdge('妻', qingkeqing);\n" + + "jiashe.addEdge('父子', jialian);\n" + + "jialian.addEdge('妻', wangxifeng);\n" + + "jialian.addEdge('妾', youerjie);\n" + + "jialian.addEdge('妾', pinger);\n" + + "jialian.addEdge('妾', qiutong);\n" + + "wangxifeng.addEdge('丫头', pinger);\n" + + "jiashe.addEdge('丫环', qiutong);\n" + + "youdajie.addEdge('姐妹', youerjie);\n" + + "jiashe.addEdge('父女', jiayingchun);\n" + + "sunshaozu.addEdge('妻', jiayingchun);\n" + + "jiazheng.addEdge('妻', wangfuren);\n" + + "jiazheng.addEdge('妾', zhaoyiniang);\n" + + "zhaoyiniang.addEdge('母子', jiahuan);\n" + + "zhaoyiniang.addEdge('母女', jiatanchun);\n" + + "wangfuren.addEdge('母女', jiayuanchun);\n" + + "wangfuren.addEdge('母子', jiazhu);\n" + + "jiazhu.addEdge('妻', liwan);\n" + + "jiazhu.addEdge('父子', jialan);\n" + + "liwan.addEdge('母子', jialan);\n" + + "wangfuren.addEdge('母子', jiabaoyu);\n" + + "jiabaoyu.addEdge('妻', xuebaochai);\n" + + "linruhai.addEdge('妻', jiamin);\n" + + "linruhai.addEdge('父女', lindaiyu);\n" + + "jiamin.addEdge('母女', lindaiyu);\n" + + "jiabaoyu.addEdge('相恋', lindaiyu);\n" + + "jiamu.addEdge('母子', jiashe);\n" + + "jiamu.addEdge('母子', jiazheng);\n" + + "jiamu.addEdge('母女', jiamin);\n" + + "jiadaishan.addEdge('父子', jiashe);\n" + + "jiadaishan.addEdge('父子', jiazheng);\n" + + "jiadaishan.addEdge('父女', jiamin);\n" + + "shihou.addEdge('父子', shigong);\n" + + "shigong.addEdge('父子', shiba);\n" + + "shigong.addEdge('父子', shinai);\n" + + "shigong.addEdge('父子', shiding);\n" + + "shigong.addEdge('父女', jiamu);\n" + + "shiba.addEdge('父女', shixiangyun);\n" + + "weiruolan.addEdge('妻', shixiangyun);\n" + + "jiabaoyu.addEdge('朋友', shixiangyun);\n" + + "xueyima.addEdge('姐妹', wangfuren);\n" + + "xueyima.addEdge('母女', xuebaochai);"; + + public static final String GREMLIN_LOAD_HLM = GREMLIN_HLM_SCHEMA + + GREMLIN_HLM_DATA; + + public static final String GREMLIN_COVID19_SCHEMA = + "\"graph.schema().propertyKey('性别').asText().ifNotExist().create();\\n" + + "graph.schema().propertyKey('年龄').asInt().ifNotExist().create();\\n" + + "graph.schema().propertyKey('防疫政策').asText().ifNotExist().create();\\n" + + "graph.schema().propertyKey('类型').asText().ifNotExist().create();\\n" + + "graph.schema().propertyKey('时间').asText().ifNotExist().create();\\n" + + "\\n" + + "graph.schema().vertexLabel('patient').properties('性别','年龄')" + + ".useCustomizeStringId().nullableKeys('性别','年龄')" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "graph.schema().vertexLabel('place')" + + ".useCustomizeStringId()" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "graph.schema().vertexLabel('city').properties('防疫政策')" + + ".useCustomizeStringId().nullableKeys('防疫政策')" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "graph.schema().vertexLabel('vehicle').properties('类型')" + + ".useCustomizeStringId().nullableKeys('类型')" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "\\n" + + "graph.schema().edgeLabel('relation')" + + ".sourceLabel('patient').targetLabel('patient')" + + ".properties('类型').nullableKeys('类型')" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "graph.schema().edgeLabel('reside')" + + ".sourceLabel('patient').targetLabel('place')" + + ".properties('时间').nullableKeys('时间')" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "graph.schema().edgeLabel('work')" + + ".sourceLabel('patient').targetLabel('place')" + + ".properties('时间').nullableKeys('时间')" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "graph.schema().edgeLabel('stay')" + + ".sourceLabel('patient').targetLabel('place')" + + ".properties('时间').nullableKeys('时间')" + + ".enableLabelIndex(false).ifNotExist().create();" + + "\\ngraph.schema().edgeLabel('comfirm')" + + ".sourceLabel('patient').targetLabel('city')" + + ".properties('时间').nullableKeys('时间')" + + ".enableLabelIndex(false).ifNotExist().create();\\n" + + "graph.schema().edgeLabel('take')" + + ".sourceLabel('patient').targetLabel('vehicle')" + + ".properties('时间').nullableKeys('时间')" + + ".enableLabelIndex(false).ifNotExist().create();\\n\\n\""; + public static String escapeId(Object id) { if (!(id instanceof String)) { return id.toString(); } - String text = (String) id; - text = StringUtils.replaceEach(text, ESCAPE_SEARCH_LIST, - ESCAPE_TARGET_LIST); - return (String) escape(text); + return (String) escape(id); } public static Object escape(Object object) { if (!(object instanceof String)) { return object; } - return StringUtils.wrap((String) object, '\''); + String text = (String) object; + text = StringUtils.replaceEach(text, ESCAPE_SEARCH_LIST, + ESCAPE_TARGET_LIST); + return StringUtils.wrap(text, '\''); } public static String optimizeLimit(String gremlin, int limit) { + // FIXME: Do not treat suffix matching as a resource-safety boundary. Decide on + // server-side timeout/result/byte limits without changing aggregation semantics. String[] rawLines = StringUtils.split(gremlin, "\n"); List newLines = new ArrayList<>(rawLines.length); for (String rawLine : rawLines) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HubbleUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HubbleUtil.java index 186f0b738..374f3a5a9 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HubbleUtil.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HubbleUtil.java @@ -18,21 +18,188 @@ package org.apache.hugegraph.util; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.time.Instant; -import java.util.Collection; +import java.time.ZoneOffset; +import java.util.TimeZone; import java.util.Date; import java.util.UUID; +import java.util.Calendar; +import java.util.Collection; import java.util.regex.Pattern; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.collections.CollectionUtils; public final class HubbleUtil { + static { + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.of("+8"))); + } + + public static final Pattern HOST_PATTERN = + Pattern.compile("(([0-9]{1,3}\\.){3}[0-9]{1,3}|" + + "([0-9A-Za-z_!~*'()-]+\\.)*[0-9A-Za-z_!~*'()-]+)$"); + private static final String DF = "yyyy-MM-dd HH:mm:ss"; + + private static final String M_FORMAT = "yyyyMMdd'T'HHmmssSSS"; + public static final DateFormat DATE_FORMAT = new SimpleDateFormat(DF); + + /** + * Isolates casts required by legacy client APIs that return raw JSON + * containers. Callers remain responsible for using the documented type. + */ + @SuppressWarnings("unchecked") + public static T uncheckedCast(Object value) { + return (T) value; + } + + public static String dateFormat() { + return DATE_FORMAT.format(new Date()); + } + + public static String dateFormatMillis() { + return (new SimpleDateFormat(M_FORMAT)).format(new Date()); + } + + public static String dateFormatMonth(Date date) { + return (new SimpleDateFormat("yyyyMM").format(date)); + } + + public static String dateFormatDay(Date date) { + return (new SimpleDateFormat("yyyyMMdd").format(date)); + } - public static final Pattern HOST_PATTERN = Pattern.compile( - "(([0-9]{1,3}\\.){3}[0-9]{1,3}|" + - "([0-9A-Za-z_!~*'()-]+\\.)*[0-9A-Za-z_!~*'()-]+)$" - ); + public static String dateFormatDay(String date) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + Date da = null; + try { + da = sdf.parse(date); + } catch (ParseException e) { + throw new RuntimeException(e); + } + return (new SimpleDateFormat("yyyy-MM-dd").format(da)); + } + + public static String dateFormatLastMonth() { + Calendar cal = Calendar.getInstance(); + int month = cal.get(Calendar.MONTH); + + cal.set(Calendar.MONTH, month - 1); + cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)); + return (new SimpleDateFormat("yyyyMM").format(cal.getTime())); + } + + public static String dateFormatLastDay() { + Calendar cal = Calendar.getInstance(); + int da = cal.get(Calendar.DATE); + cal.set(Calendar.DATE, da - 1); + return (new SimpleDateFormat("yyyyMMdd").format(cal.getTime())); + } + + /** + * 获取当前时间往前推7天的时间戳数组(单位/秒) + * @return 包含前7天时间戳和当前时间戳的数组 + */ + public static long[] getTimestampsBefore7Days() { + Calendar cal = Calendar.getInstance(); + long currentTimestamp = cal.getTimeInMillis(); + // 将时间设置为当前时间往前推24小时 + cal.add(Calendar.HOUR_OF_DAY, -24 * 7); + long timestampBefore24Hours = cal.getTimeInMillis(); + return new long[]{timestampBefore24Hours / 1000, + currentTimestamp / 1000}; + } + + /** + * 获取当前时间往前推24小时的时间戳数组(单位/秒) + * @return 包含前24小时时间戳和当前时间戳的数组 + */ + public static long[] getTimestampsBefore24Hours() { + Calendar cal = Calendar.getInstance(); + long currentTimestamp = cal.getTimeInMillis(); + // 将时间设置为当前时间往前推24小时 + cal.add(Calendar.HOUR_OF_DAY, -24); + long timestampBefore24Hours = cal.getTimeInMillis(); + return new long[]{timestampBefore24Hours / 1000, + currentTimestamp / 1000}; + } + + public static long[] getTimestampsBefore24Hours(String time) { + Date date = null; + try { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); + date = dateFormat.parse(time); + } catch (ParseException e) { + throw new RuntimeException(e); + } + return getTimestampsBefore24Hours(date.getTime()); + } + + /** + * 获取timestamp时间往前推24小时的时间戳数组(单位/秒) + * @param timestamp 给定时间戳(单位/ms) + * @return 包含前24小时时间戳和当前时间戳的数组 + */ + private static long[] getTimestampsBefore24Hours(long timestamp) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(timestamp); + // 将时间设置为当前时间往前推24小时 + cal.add(Calendar.HOUR_OF_DAY, -24); + return new long[]{cal.getTimeInMillis() / 1000, timestamp / 1000}; + } + + /** + * 获取给定日期所在周的周一和周日的时间戳数组(单位/秒) + * @param date 给定日期 + * @return 包含周一和周日时间戳的数组 + */ + public static long[] getWeekTimestamps(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + resetTime(cal); + long startOfWeek = cal.getTimeInMillis(); + // 将日期调整到下周一 + cal.add(Calendar.DAY_OF_WEEK, 7); + resetTime(cal); + cal.add(Calendar.SECOND, -1); + long endOfWeek = cal.getTimeInMillis(); + return new long[]{startOfWeek / 1000, endOfWeek / 1000}; + } + + /** + * 获取给定日期所在月份的第一天和最后一天的时间戳数组(单位/秒) + * @param date 给定日期 + * @return 包含月份第一天和最后一天时间戳的数组 + */ + public static long[] getMonthTimestamps(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + // 将日期调整到该月的第一天 + cal.set(Calendar.DAY_OF_MONTH, 1); + resetTime(cal); + long startOfMonth = cal.getTimeInMillis(); + // 将日期调整到下个月的第一天 + cal.add(Calendar.MONTH, 1); + resetTime(cal); + // 将日期调整到本月的最后一天 + cal.add(Calendar.SECOND, -1); + // 获取月份的结束时间戳 + long endOfMonth = cal.getTimeInMillis(); + return new long[]{startOfMonth, endOfMonth}; + } + + /** + * 重置时间为当天的开始时间(即00:00:00) + */ + private static void resetTime(Calendar cal) { + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + } public static Date nowDate() { return new Date(); @@ -56,4 +223,8 @@ public static String generateSimpleId() { public static String md5(String rawText) { return DigestUtils.md5Hex(rawText); } + + public static String md5Secret(String rawText) { + return md5("a1p" + md5(rawText).substring(5, 15) + "ck0").substring(1, 17); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java index e0bb4238b..f3e6b7f34 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java @@ -20,17 +20,15 @@ import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.GraphConnection; import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.exception.GenericException; import org.apache.hugegraph.exception.ServerException; -import org.apache.hugegraph.rest.ClientException; -import org.apache.hugegraph.structure.gremlin.Result; -import org.apache.hugegraph.structure.gremlin.ResultSet; import org.springframework.web.util.UriComponentsBuilder; +import org.apache.hugegraph.rest.ClientException; import com.google.common.collect.ImmutableSet; public final class HugeClientUtil { @@ -42,31 +40,36 @@ public final class HugeClientUtil { ); public static HugeClient tryConnect(GraphConnection connection) { + String graphSpace = connection.getGraphSpace(); String graph = connection.getGraph(); String host = connection.getHost(); Integer port = connection.getPort(); + String token = normalizeToken(connection.getToken()); String username = connection.getUsername(); String password = connection.getPassword(); int timeout = connection.getTimeout(); - String protocol = connection.getProtocol() == null ? - DEFAULT_PROTOCOL : connection.getProtocol(); + String protocol = StringUtils.isEmpty(connection.getProtocol()) ? + DEFAULT_PROTOCOL : + connection.getProtocol(); String trustStoreFile = connection.getTrustStoreFile(); String trustStorePassword = connection.getTrustStorePassword(); String url = UriComponentsBuilder.newInstance() - .scheme(protocol).host(host).port(port).toUriString(); + .scheme(protocol) + .host(host).port(port) + .toUriString(); if (username == null) { username = ""; password = ""; } HugeClient client; + boolean skipRequiredChecks = (graph == null || graph.isEmpty()); try { - client = HugeClient.builder(url, graph) + client = HugeClient.builder(url, graphSpace, graph, skipRequiredChecks) + .configToken(token) .configUser(username, password) - // TODO: change it to connTimeout & readTimeout .configTimeout(timeout) .configSSL(trustStoreFile, trustStorePassword) - .configHttpBuilder(http -> http.followRedirects(false)) .build(); } catch (IllegalStateException e) { String message = e.getMessage(); @@ -74,7 +77,7 @@ public static HugeClient tryConnect(GraphConnection connection) { throw new ExternalException("client-server.version.unmatched", e); } if (message != null && (message.startsWith("Error loading trust store from") || - message.startsWith("Cannot find trust store file"))) { + message.startsWith("Cannot find trust store file"))) { throw new ExternalException("https.load.truststore.error", e); } throw e; @@ -82,12 +85,15 @@ public static HugeClient tryConnect(GraphConnection connection) { String message = e.getMessage(); if (Constant.STATUS_UNAUTHORIZED == e.status() || (message != null && message.startsWith("Authentication"))) { - throw new ExternalException("graph-connection.username-or-password.incorrect", e); + throw new ExternalException( + "graph-connection.username-or-password.incorrect", e); } - if (message != null && message.contains("Invalid syntax for username and password")) { - throw new ExternalException("graph-connection.missing-username-password", e); + if (message != null && message.contains("Invalid syntax for " + + "username and password")) { + throw new ExternalException( + "graph-connection.missing-username-password", e); } - throw new GenericException(e); + throw e; } catch (ClientException e) { Throwable cause = e.getCause(); if (cause == null || cause.getMessage() == null) { @@ -100,34 +106,22 @@ public static HugeClient tryConnect(GraphConnection connection) { message.contains("Host name may not be null")) { throw new ExternalException("service.unknown-host", e, host); } else if (message.contains("")) { - throw new ExternalException("service.suspected-web", e, host, port); + throw new ExternalException("service.suspected-web", + e, host, port); } throw e; - } catch (Exception e) { - throw new GenericException(e); } - try { - ResultSet rs = client.gremlin().gremlin("g.V().limit(1)").execute(); - rs.iterator().forEachRemaining(Result::getObject); - } catch (ServerException e) { - if (Constant.STATUS_UNAUTHORIZED == e.status()) { - throw new ExternalException("graph-connection.username-or-password.incorrect", e); - } - String message = e.message(); - if (message != null && message.contains("Could not rebind [g]")) { - throw new ExternalException("graph-connection.graph.unexist", e, graph, host, port); - } - if (!isAcceptable(message)) { - throw e; - } - } catch (Exception e) { - client.close(); - throw e; - } return client; } + private static String normalizeToken(String token) { + if (StringUtils.isBlank(token) || token.startsWith(" ")) { + return token; + } + return " " + token; + } + private static boolean isAcceptable(String message) { if (message == null) { return false; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/PageUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/PageUtil.java index 3f4832276..523410fe6 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/PageUtil.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/PageUtil.java @@ -27,7 +27,28 @@ public final class PageUtil { + public static final int MAX_PAGE_SIZE = 500; + + public static void checkPage(int pageNo, int pageSize) { + boolean invalidSize = pageSize != -1 && + (pageSize < 1 || pageSize > MAX_PAGE_SIZE); + if (pageNo < 1 || invalidSize) { + throw new IllegalArgumentException( + "page_no must be >= 1 and page_size must be -1 or between 1 " + + "and " + MAX_PAGE_SIZE); + } + } + + public static void checkPositivePage(int pageNo, int pageSize) { + if (pageNo < 1 || pageSize < 1 || pageSize > MAX_PAGE_SIZE) { + throw new IllegalArgumentException( + "page_no must be >= 1 and page_size must be between 1 and " + + MAX_PAGE_SIZE); + } + } + public static IPage page(List entities, int pageNo, int pageSize) { + checkPage(pageNo, pageSize); // Regard page no < 1 as 1 int current = pageNo > 1 ? pageNo : 1; int pages; @@ -43,7 +64,12 @@ public static IPage page(List entities, int pageNo, int pageSize) { } else { pages = 0; // Return all entities when page size is negative - records = pageSize < 0 ? entities : Collections.emptyList(); + if (pageSize < 0) { + records = entities; + pageSize = entities.size(); + } else { + records = Collections.emptyList(); + } } Page page = new Page<>(current, pageSize, entities.size(), true); @@ -52,4 +78,18 @@ public static IPage page(List entities, int pageNo, int pageSize) { page.setPages(pages); return page; } + + public static IPage newPage(List records, int pageNo, + int pageSize, int total) { + checkPositivePage(pageNo, pageSize); + int current = pageNo; + int pages = total == 0 ? 0 : (total + pageSize - 1) / pageSize; + + Page page = new Page<>(current, pageSize, total, true); + page.setRecords(records); + page.setOrders(Collections.emptyList()); + page.setPages(pages); + + return page; + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/SQLUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/SQLUtil.java index 98dc5de7e..8ba416425 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/SQLUtil.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/SQLUtil.java @@ -19,6 +19,7 @@ package org.apache.hugegraph.util; import org.apache.commons.lang3.StringUtils; + import org.apache.hugegraph.common.Constant; public final class SQLUtil { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/UrlUtil.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/UrlUtil.java new file mode 100644 index 000000000..eeca7b2d9 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/UrlUtil.java @@ -0,0 +1,125 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +public class UrlUtil { + + public static Host parseHost(String url) { + if (url == null || url.trim().isEmpty()) { + throw new IllegalArgumentException("Invalid HTTP host: " + url); + } + + String text = url.trim(); + try { + URI uri = parseUri(text); + String host = normalizeHost(uri.getHost()); + int port = uri.getPort(); + String scheme = uri.getScheme(); + + if (host == null || host.isEmpty()) { + throw new IllegalArgumentException("Invalid HTTP host: " + text); + } + + if (port < 0 && hasEmptyExplicitPort(uri)) { + throw new IllegalArgumentException("Invalid HTTP host: " + text); + } + if (port < 0) { + port = defaultPort(scheme); + } + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Invalid HTTP host: " + text); + } + + return new Host(host, port, scheme); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid HTTP host: " + text, e); + } + } + + private static URI parseUri(String text) throws URISyntaxException { + if (hasScheme(text) || text.startsWith("//")) { + return new URI(text); + } + return new URI("//" + text); + } + + private static boolean hasScheme(String text) { + int schemeIdx = text.indexOf("://"); + if (schemeIdx <= 0 || !Character.isLetter(text.charAt(0))) { + return false; + } + + for (int i = 1; i < schemeIdx; i++) { + char c = text.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '+' && c != '-' && + c != '.') { + return false; + } + } + return true; + } + + private static int defaultPort(String scheme) { + if (scheme == null) { + return -1; + } + switch (scheme.toLowerCase(Locale.ROOT)) { + case "http": + return 80; + case "https": + return 443; + default: + return -1; + } + } + + private static boolean hasEmptyExplicitPort(URI uri) { + String authority = uri.getRawAuthority(); + if (authority == null) { + return false; + } + int userInfoIdx = authority.lastIndexOf('@'); + String hostPort = authority.substring(userInfoIdx + 1); + return hostPort.endsWith(":"); + } + + private static String normalizeHost(String host) { + if (host != null && host.startsWith("[") && host.endsWith("]")) { + return host.substring(1, host.length() - 1); + } + return host; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Host { + protected String host; + protected int port; + protected String scheme; + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/version/HubbleVersion.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/version/HubbleVersion.java index f6979965c..3bfb7b2bb 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/version/HubbleVersion.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/version/HubbleVersion.java @@ -32,13 +32,13 @@ public final class HubbleVersion { // The second parameter of Version.of() is for IDE running without JAR public static final Version VERSION = Version.of(HubbleVersion.class, - "1.5.0"); + "3.5.0"); public static void check() { // Check version of hugegraph-common & hugegraph-client - VersionUtil.check(CommonVersion.VERSION, "1.6.0", "1.7", + VersionUtil.check(CommonVersion.VERSION, "1.6.0", "1.9", CommonVersion.NAME); - VersionUtil.check(ClientVersion.VERSION, "1.8.0", "1.9", + VersionUtil.check(ClientVersion.VERSION, "3.5.0", "3.6", ClientVersion.NAME); } } diff --git a/hugegraph-hubble/hubble-be/src/main/resources/application.properties b/hugegraph-hubble/hubble-be/src/main/resources/application.properties index 851a2746e..bdbdb2a4d 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/application.properties +++ b/hugegraph-hubble/hubble-be/src/main/resources/application.properties @@ -1,4 +1,5 @@ # +# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with this # work for additional information regarding copyright ownership. The ASF @@ -17,10 +18,25 @@ info.app.name=hugegraph-hubble info.app.version=v1.2 +spring.profiles.active=dev -# web static file path -spring.resources.static-locations=classpath:/ui/ +# SECURITY: Production or public deployments must terminate HTTPS before Hubble. +# Never expose Hubble's plain HTTP listener directly to an untrusted network. +# Trust forwarding headers only from Tomcat's internal-proxy ranges. Configure +# server.tomcat.internal-proxies explicitly when the HTTPS proxy uses other ranges. +server.use-forward-headers=true +# Keep an active browser session alive; Servlet access refreshes the idle timer. +server.servlet.session.timeout=48h + +# Login brute-force protection: first three failures pass normally, then back off. +auth.login.failure-threshold=4 +auth.login.initial-backoff-seconds=5 +auth.login.max-backoff-seconds=600 +auth.login.max-tracked-principals=10000 +# web static file path, local h2 database or remote mysql for you to choose +# local h2 database +spring.resources.static-locations=classpath:/ui/ spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:file:./db;DB_CLOSE_ON_EXIT=FALSE spring.datasource.username=sa @@ -28,6 +44,12 @@ spring.datasource.password= spring.datasource.schema=classpath:database/schema.sql spring.datasource.data=classpath:database/data.sql +# remote mysql +#spring.datasource.url=jdbc:mysql://{ip}:{port}/{database}?serverTimezone=Asia/Shanghai +#spring.datasource.username={username} +#spring.datasource.password={password} +#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=15 spring.datasource.hikari.auto-commit=true @@ -39,6 +61,7 @@ spring.datasource.hikari.connection-test-query=SELECT 1 spring.messages.encoding=UTF-8 spring.messages.basename=i18n/messages +spring.messages.fallback-to-system-locale=false spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 @@ -62,8 +85,12 @@ mybatis.configuration.default-statement-timeout=600 management.endpoints.web.exposure.include=* +# close es health check +management.health.elasticsearch.enabled=false + logging.level.org.springframework=WARN logging.level.org.apache.hugegraph.mapper=INFO logging.level.org.apache.hugegraph.service=INFO logging.file=logs/hugegraph-hubble.log +#logging.config=file:./conf/log4j2.xml logging.file.max-size=10MB diff --git a/hugegraph-hubble/hubble-be/src/main/resources/database/data.sql b/hugegraph-hubble/hubble-be/src/main/resources/database/data.sql index b6f344278..bab147963 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/database/data.sql +++ b/hugegraph-hubble/hubble-be/src/main/resources/database/data.sql @@ -1,19 +1,20 @@ SELECT 1; /* + * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + * 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. */ -- INSERT INTO `graph_connection`(name, graph, host, port, timeout, create_time) VALUES ('s', 'hugegraph', 'localhost', 8080, 60, sysdate); diff --git a/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql b/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql index c4c646f51..fbfbac687 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql +++ b/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql @@ -1,3 +1,39 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -23,41 +59,64 @@ CREATE TABLE IF NOT EXISTS `user_info` ( UNIQUE (`username`) ); -CREATE TABLE IF NOT EXISTS `graph_connection` ( - `id` INT NOT NULL AUTO_INCREMENT, - `name` VARCHAR(48) NOT NULL, - `graph` VARCHAR(48) NOT NULL, - `host` VARCHAR(48) NOT NULL DEFAULT 'localhost', - `port` INT NOT NULL DEFAULT '8080', - `timeout` INT NOT NULL, - `username` VARCHAR(48), - `password` VARCHAR(48), - `enabled` BOOLEAN NOT NULL DEFAULT true, - `disable_reason` VARCHAR(65535) NOT NULL DEFAULT '', - `create_time` DATETIME(6) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE (`name`), - UNIQUE (`graph`, `host`, `port`) +-- DROP TABLE IF EXISTS `app_info`; +CREATE TABLE IF NOT EXISTS `app_info`( + `graph_name` varchar(255) DEFAULT NULL, + `app_name` varchar(255) NOT NULL, + `app_type` varchar(255) NOT NULL, + `count_query` text DEFAULT NULL, + `distribution_query` text DEFAULT NULL, + `description` text DEFAULT NULL, + PRIMARY KEY (`graph_name`, `app_name`, `app_type`) ); +-- DROP TABLE IF EXISTS `execute_history`; CREATE TABLE IF NOT EXISTS `execute_history` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, - `async_id` LONG NOT NULL DEFAULT 0, + `conn_id` INT, + `graphspace` VARCHAR(48) NOT NULL, + `graph` VARCHAR(48) NOT NULL, + `async_id` LONG NOT NULL, `execute_type` TINYINT NOT NULL, - `content` VARCHAR(65535) NOT NULL, + `content` TEXT NOT NULL, + `text` TEXT NOT NULL, `execute_status` TINYINT NOT NULL, + `failure_reason` VARCHAR(64) DEFAULT NULL, `async_status` TINYINT NOT NULL DEFAULT 0, `duration` LONG NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`) -); + ); + CREATE INDEX IF NOT EXISTS `execute_history_conn_id` ON `execute_history`(`conn_id`); + +// DROP TABLE IF EXISTS `edit_history`; +CREATE TABLE IF NOT EXISTS `edit_history` +( + `id` int NOT NULL AUTO_INCREMENT, + `graphspace` varchar(255) DEFAULT NULL, + `graph` varchar(255) DEFAULT NULL, + `element_id` varchar(255) DEFAULT NULL, + `label` varchar(255) DEFAULT NULL, + `property_num` int DEFAULT NULL, + `option_type` varchar(255) DEFAULT NULL, + `option_time` datetime DEFAULT NULL, + `option_person` varchar(255) DEFAULT NULL, + `content` longtext, + PRIMARY KEY (`id`) +); + +CREATE INDEX IF NOT EXISTS `idx_graphspace_graph` ON `edit_history` (`graphspace`, `graph`); + + CREATE TABLE IF NOT EXISTS `gremlin_collection` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, + `conn_id` INT, + `graphspace` VARCHAR(48) NOT NULL, + `graph` VARCHAR(48) NOT NULL, `name` VARCHAR(48) NOT NULL, + `type` VARCHAR(48) NOT NULL, `content` VARCHAR(65535) NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`), @@ -67,10 +126,12 @@ CREATE INDEX IF NOT EXISTS `gremlin_collection_conn_id` ON `gremlin_collection`( CREATE TABLE IF NOT EXISTS `file_mapping` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, + `conn_id` INT, + `graphspace` VARCHAR(48) NOT NULL, + `graph` VARCHAR(48) NOT NULL, `job_id` INT NOT NULL DEFAULT 0, `name` VARCHAR(128) NOT NULL, - `path` VARCHAR(256) NOT NULL, + `path` VARCHAR(2048) NOT NULL, `total_lines` LONG NOT NULL, `total_size` LONG NOT NULL, `file_status` TINYINT NOT NULL DEFAULT 0, @@ -87,7 +148,9 @@ CREATE INDEX IF NOT EXISTS `file_mapping_conn_id` ON `file_mapping`(`conn_id`); CREATE TABLE IF NOT EXISTS `load_task` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, + `conn_id` INT, + `graphspace` VARCHAR(48) NOT NULL, + `graph` VARCHAR(48) NOT NULL, `job_id` INT NOT NULL DEFAULT 0, `file_id` INT NOT NULL, `file_name` VARCHAR(128) NOT NULL, @@ -105,7 +168,9 @@ CREATE TABLE IF NOT EXISTS `load_task` ( CREATE TABLE IF NOT EXISTS `job_manager` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL DEFAULT 0, + `conn_id` INT DEFAULT 0, + `graphspace` VARCHAR(48) NOT NULL, + `graph` VARCHAR(48) NOT NULL, `job_name` VARCHAR(100) NOT NULL DEFAULT '', `job_remarks` VARCHAR(200) NOT NULL DEFAULT '', `job_size` LONG NOT NULL DEFAULT 0, @@ -114,12 +179,14 @@ CREATE TABLE IF NOT EXISTS `job_manager` ( `update_time` DATETIME(6) NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`), - UNIQUE (`job_name`, `conn_id`) + UNIQUE (`job_name`, `graphspace`, `graph`) ); CREATE TABLE IF NOT EXISTS `async_task` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL DEFAULT 0, + `conn_id` INT DEFAULT 0, + `graphspace` VARCHAR(48) NOT NULL, + `graph` VARCHAR(48) NOT NULL, `task_id` INT NOT NULL DEFAULT 0, `task_name` VARCHAR(100) NOT NULL DEFAULT '', `task_reason` VARCHAR(200) NOT NULL DEFAULT '', @@ -134,3 +201,12 @@ CREATE TABLE IF NOT EXISTS `async_task` ( CREATE INDEX IF NOT EXISTS `load_task_conn_id` ON `load_task`(`conn_id`); CREATE INDEX IF NOT EXISTS `load_task_file_id` ON `load_task`(`file_id`); + +CREATE TABLE IF NOT EXISTS `datasource` ( + `id` INT NOT NULL AUTO_INCREMENT, + `datasource_name` VARCHAR(128) NOT NULL, + `datasource_config` TEXT NOT NULL, + `creator` VARCHAR(64) NOT NULL DEFAULT '', + `create_time` DATETIME(6) NOT NULL, + PRIMARY KEY (`id`) +); diff --git a/hugegraph-hubble/hubble-be/src/main/resources/hugegraph-hubble.properties b/hugegraph-hubble/hubble-be/src/main/resources/hugegraph-hubble.properties index 28dc5337c..ec77a0cf6 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/hugegraph-hubble.properties +++ b/hugegraph-hubble/hubble-be/src/main/resources/hugegraph-hubble.properties @@ -1,4 +1,5 @@ # +# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with this # work for additional information regarding copyright ownership. The ASF @@ -17,15 +18,55 @@ server.host=localhost server.port=8088 +# SECURITY: Keep this HTTP listener private. Production/public access requires an +# HTTPS reverse proxy with secure session-cookie handling and no direct port bypass. + +# SECURITY: The Kubernetes token API is hard-disabled in code. Do not restore it +# without server-side privileged authorization and dedicated security review. graph_connection.ip_white_list=[*] graph_connection.port_white_list=[-1] +client.request_timeout=310 + gremlin.suffix_limit=250 gremlin.vertex_degree_limit=100 gremlin.edges_total_limit=500 gremlin.batch_query_ids=100 +langchain.python_path=python3 +langchain.script_dir=langchaincode +langchain.script_allowlist=[excute_langchain.py] +langchain.execute_timeout=30 + server.protocol=http #ssl.client_truststore_file= #ssl.client_truststore_password= + +cluster=hg +idc=bddwd + +# ===== Deployment Mode ===== +# Set to false for standalone RocksDB mode (no PD dependency) +pd.enabled=false +# Direct server URL, only used when pd.enabled=false +server.direct_url=http://127.0.0.1:8080 + +# pd +pd.peers=127.0.0.1:8686 +pd.server=127.0.0.1:8620 + +# dashboard +dashboard.address=127.0.0.1:8092 +# BOTH, NODE_PORT, DDS +route.type=NODE_PORT + +# Set monitor url +monitor.url= +prometheus.url=http://127.0.0.1:8090 + +# ES +es.urls= + +proxy.servlet_url=/api/v1.3/ingest/* +proxy.target_url=WhatURLAtHere diff --git a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties index ddbb082d4..9f39cf5e0 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties +++ b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties @@ -1,4 +1,5 @@ # +# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with this # work for additional information regarding copyright ownership. The ASF @@ -23,6 +24,14 @@ common.param.must-be-null=The param {0} must be null common.param.should-belong-to=The param {0} should belong to {1} common.name-order.invalid=The param name_order either not set or set to [asc, desc], but got {0} common.time-order.invalid=The param time_order either not set or set to [asc, desc], but got {0} +request.parameter.required=The request parameter ''{0}'' is required +server.capability.pd-status.unavailable=PD status is unavailable because this HugeGraph Server does not expose the required API +server.capability.hstore-status.unavailable=HStore status is unavailable because this HugeGraph Server does not expose the required API +auth.user.batch-create.failed=Failed to create users: {0} +auth.user.import-file.failed=Failed to prepare the uploaded user import file +auth.user.import-csv.failed=Failed to parse the uploaded user CSV file +k8s.token.file.not-exist=The Kubernetes token file does not exist +k8s.token.directory.unavailable=The Kubernetes token directory is unavailable common.name-time-order.conflict=The param name_order and time_order cannot set at same time common.param.path-id-should-same-as-body=The id in path({0}) must be same as request body({1}) if it exists @@ -55,6 +64,11 @@ graph.vertex.all-nonnullable-prop.should-be-setted=The all vertex non nullable p graph.edge.link-unmatched-vertex=The edge {0} can only be linked as {1} -> {2}, actual is {3} -> {4} graph.edge.all-nonnullable-prop.should-be-setted=The all edge non nullable properties should be setted graph.property.convert.failed=Failed to convert property {1}(key: {0}) +graph.import.invalid-json=The import file is not valid JSON +graph.import.file.exceed-limit=The import file size {0} exceeds limit {1} +graph.import.missing-field=The import field {0} is required +graph.import.field-should-array=The import field {0} should be an array +graph.import.vertex.duplicate-id=The import vertex id {0} is duplicated gremlin-collection.name.unmatch-regex=Invalid gremlin statement name, valid name is up to 48 alpha-numeric characters and underscores gremlin-collection.content.invalid=Invalid gremlin statemnt {0} @@ -202,3 +216,12 @@ license.verfiy.mac-unmatch-ip=Failed to get mac address for IP {0} license.verify.mac.unauthorized=The hugegraph-hubble's mac {0} doesn't match the authorized {1} https.load.truststore.error=Failed to load https trusted certificate + +service.no-available=No service available +service.graphspace.no-available=No service available +service.default.no-available=No service avaialbe under namespace named 'DEFAULT' +service.url.parse.error=Parse host info ({0}) error?please check the url in \ + service config + +service.manual.disable.modify=Disallown modify manual service +auth.login.throttled=Too many login failures. Please retry in {0} seconds diff --git a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties index 7c333d384..ef9fc53a7 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties +++ b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties @@ -1,4 +1,5 @@ # +# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with this # work for additional information regarding copyright ownership. The ASF @@ -23,6 +24,14 @@ common.param.must-be-null=参数 {0} 必须为 null common.param.should-belong-to=参数 {0} 的取值范围为 {1} common.name-order.invalid=参数 name_order 要么不设置,要么设置为 asc 或 desc,但实际为 {0} common.time-order.invalid=参数 time_order 要么不设置,要么设置为 asc 或 desc,但实际为 {0} +request.parameter.required=缺少必填请求参数“{0}” +server.capability.pd-status.unavailable=当前 HugeGraph Server 未提供所需接口,无法获取 PD 状态 +server.capability.hstore-status.unavailable=当前 HugeGraph Server 未提供所需接口,无法获取 HStore 状态 +auth.user.batch-create.failed=以下用户创建失败:{0} +auth.user.import-file.failed=无法准备上传的用户导入文件 +auth.user.import-csv.failed=无法解析上传的用户 CSV 文件 +k8s.token.file.not-exist=Kubernetes token 文件不存在 +k8s.token.directory.unavailable=Kubernetes token 目录不可用 common.name-time-order.conflict=参数 name_order 和 time_order 不能同时设置 common.param.path-id-should-same-as-body=当请求体中的 id({1}) 存在时,必须与路径中的 ({0}) 相同 @@ -55,6 +64,11 @@ graph.vertex.all-nonnullable-prop.should-be-setted=顶点的所有非空属性 graph.edge.link-unmatched-vertex=边 {0} 只能连接 {1} -> {2},但实际连到了 {3} -> {4} graph.edge.all-nonnullable-prop.should-be-setted=边的所有非空属性都应该被设置 graph.property.convert.failed=转换属性 {1}(key: {0}) 失败 +graph.import.invalid-json=导入文件不是合法 JSON +graph.import.file.exceed-limit=导入文件大小 {0} 超过限制 {1} +graph.import.missing-field=导入字段 {0} 必填 +graph.import.field-should-array=导入字段 {0} 应为数组 +graph.import.vertex.duplicate-id=导入顶点 id {0} 重复 gremlin-collection.name.unmatch-regex=语句名不合法,语句名允许字母、数字、中文、下划线,最多 48 个字符 gremlin-collection.content.invalid=待收藏语句 {0} 不合法 @@ -202,3 +216,9 @@ license.verfiy.mac-unmatch-ip=hugegraph-hubble 机器的 MAC 与 IP {0} 不匹 license.verify.mac.unauthorized=hugegraph-hubble 机器的 MAC {0} 不在已授权范围内 {1} https.load.truststore.error=https 可信证书加载失败 + +service.no-available=当前无可用服务 +service.default.no-available=DEFAULT命名空间下的DEFAULT服务无法使用 +service.url.parse.error="无法解析的主机名或 IP ({0}) ,请修改相关服务的URL配置" +service.manual.disable.modify=禁止修改手动启动的图服务 +auth.login.throttled=登录失败次数过多,请在 {0} 秒后重试 diff --git a/hugegraph-hubble/hubble-be/src/main/resources/log4j2.xml b/hugegraph-hubble/hubble-be/src/main/resources/log4j2.xml index 3410533ba..e9551e294 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/log4j2.xml +++ b/hugegraph-hubble/hubble-be/src/main/resources/log4j2.xml @@ -1,75 +1,50 @@ - - - UTF-8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%thread] --- %logger{35} : %msg%n + + + + + + logs/hugegraph-text2gremlin.log + + hugegraph-text2gremlin.log.%d{yyyy-MM-dd} + + + %d{yyyy-MM-dd HH:mm:ss.SSS},%msg%n + + + + + + + + + + + + - - - - - diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java new file mode 100644 index 000000000..e055a71ed --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java @@ -0,0 +1,253 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.ingest; + +import java.io.File; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.After; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.ProxyServletConfiguration; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.enums.FileMappingStatus; +import org.apache.hugegraph.entity.enums.JobStatus; +import org.apache.hugegraph.entity.enums.LoadStatus; +import org.apache.hugegraph.entity.GraphConnection; +import org.apache.hugegraph.entity.load.Datasource; +import org.apache.hugegraph.entity.load.FileMapping; +import org.apache.hugegraph.entity.load.JobManager; +import org.apache.hugegraph.entity.load.LoadTask; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.load.DatasourceService; +import org.apache.hugegraph.service.load.FileMappingService; +import org.apache.hugegraph.service.load.JobManagerService; +import org.apache.hugegraph.service.load.LoadTaskService; +import org.apache.hugegraph.testutil.Assert; + +public class IngestControllerTest { + + @After + public void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void testCreateFileTaskSavesMappingAndStartsLoader() + throws Exception { + Path uploadRoot = Files.createTempDirectory("hubble-ingest-root"); + Path dataFile = uploadRoot.resolve("data.csv"); + Files.write(dataFile, Collections.singletonList("marko")); + + TestIngestController controller = new TestIngestController(); + HugeConfig config = this.mockConfig(uploadRoot); + DatasourceService datasourceService = Mockito.mock(DatasourceService.class); + JobManagerService jobService = Mockito.mock(JobManagerService.class); + FileMappingService fileMappingService = Mockito.mock(FileMappingService.class); + LoadTaskService loadTaskService = Mockito.mock(LoadTaskService.class); + this.setField(controller, "config", config); + this.setField(controller, "datasourceService", datasourceService); + this.setField(controller, "jobManagerService", jobService); + this.setField(controller, "fileMappingService", fileMappingService); + this.setField(controller, "loadTaskService", loadTaskService); + + Datasource datasource = new Datasource(); + datasource.setId(1); + datasource.setDatasourceName("local"); + Map datasourceConfig = new HashMap<>(); + datasourceConfig.put("type", "FILE"); + datasourceConfig.put("path", dataFile.toString()); + datasourceConfig.put("format", "CSV"); + datasourceConfig.put("header", Collections.singletonList("name")); + datasource.setDatasourceConfig(datasourceConfig); + Mockito.when(datasourceService.get(1)).thenReturn(datasource); + Mockito.when(fileMappingService.requirePathUnderUploadRoot( + dataFile.toString())).thenReturn(dataFile.toFile()); + Mockito.doAnswer(invocation -> { + invocation.getArgument(0, JobManager.class).setId(7); + return null; + }).when(jobService).save(Mockito.any(JobManager.class)); + Mockito.doAnswer(invocation -> { + invocation.getArgument(0, FileMapping.class).setId(8); + return null; + }).when(fileMappingService).save(Mockito.any(FileMapping.class)); + Mockito.when(loadTaskService.start(Mockito.any(GraphConnection.class), + Mockito.any(FileMapping.class), + Mockito.any(HugeClient.class))) + .thenReturn(LoadTask.builder().id(9).build()); + this.bindRequestSession(); + + Response response = controller.createTask(this.request(dataFile)); + + Assert.assertEquals(Constant.STATUS_OK, response.getStatus()); + ArgumentCaptor mappingCaptor = + ArgumentCaptor.forClass(FileMapping.class); + Mockito.verify(fileMappingService).save(mappingCaptor.capture()); + FileMapping mapping = mappingCaptor.getValue(); + Assert.assertEquals(7, mapping.getJobId().intValue()); + Assert.assertEquals(FileMappingStatus.COMPLETED, + mapping.getFileStatus()); + Assert.assertEquals(Collections.singletonList("name"), + mapping.getFileSetting().getColumnNames()); + Assert.assertEquals(Collections.singletonList("name"), + mapping.getVertexMappings().iterator().next() + .getIdFields()); + Mockito.verify(loadTaskService).start(Mockito.any(GraphConnection.class), + Mockito.eq(mapping), + Mockito.any(HugeClient.class)); + ArgumentCaptor jobCaptor = + ArgumentCaptor.forClass(JobManager.class); + Mockito.verify(jobService).update(jobCaptor.capture()); + Assert.assertEquals(JobStatus.LOADING, jobCaptor.getValue() + .getJobStatus()); + } + + @Test + public void testProxyServletSkipsPlaceholderTarget() throws Exception { + ProxyServletConfiguration proxy = new ProxyServletConfiguration(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PROXY_SERVLET_URL)) + .thenReturn("/api/v1.3/ingest/*"); + Mockito.when(config.get(HubbleOptions.PROXY_TARGET_URL)) + .thenReturn("WhatURLAtHere"); + this.setField(proxy, "config", config); + + Assert.assertNull(proxy.servletRegistrationBean()); + } + + @Test + public void testJobListRateDoesNotBecomeInfinityForSubSecondTask() + throws Exception { + TestIngestController controller = new TestIngestController(); + LoadTaskService loadTaskService = Mockito.mock(LoadTaskService.class); + this.setField(controller, "loadTaskService", loadTaskService); + + LoadTask task = LoadTask.builder() + .id(9) + .jobId(7) + .status(LoadStatus.SUCCEED) + .fileReadLines(3L) + .lastDuration(63L) + .currDuration(0L) + .build(); + Mockito.when(loadTaskService.taskListByJob(7)) + .thenReturn(Collections.singletonList(task)); + + Response response = controller.jobList(7, 1, 10); + + Assert.assertEquals(Constant.STATUS_OK, response.getStatus()); + @SuppressWarnings("unchecked") + Map data = (Map) response.getData(); + @SuppressWarnings("unchecked") + List records = + (List) data.get("records"); + IngestController.JobMetricsVO metrics = records.get(0).jobMetrics; + Assert.assertEquals(3, (int) metrics.avgRate); + Assert.assertEquals(0, (int) metrics.curRate); + Assert.assertEquals(63L, metrics.totalTime); + } + + private IngestController.IngestTaskRequest request(Path dataFile) { + IngestController.IngestTaskRequest request = + new IngestController.IngestTaskRequest(); + request.taskName = "load-person"; + request.datasourceId = 1; + request.taskScheduleType = "ONCE"; + request.ingestionOption = new IngestController.IngestionOption(); + request.ingestionOption.graphspace = "DEFAULT"; + request.ingestionOption.graph = "hugegraph"; + request.ingestionMapping = new IngestController.IngestionMapping(); + IngestController.IngestStruct struct = + new IngestController.IngestStruct(); + struct.input = new HashMap<>(); + struct.input.put("path", dataFile.toString()); + struct.input.put("header", Collections.singletonList("name")); + Map vertex = new HashMap<>(); + vertex.put("label", "person"); + vertex.put("id", "name"); + vertex.put("field_mapping", Map.of("name", "name")); + vertex.put("null_values", Collections.singletonList("")); + struct.vertices = Collections.singletonList(vertex); + struct.edges = Collections.emptyList(); + request.ingestionMapping.structs = Collections.singletonList(struct); + return request; + } + + private HugeConfig mockConfig(Path uploadRoot) { + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.UPLOAD_FILE_LOCATION)) + .thenReturn(uploadRoot.toString()); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(false); + Mockito.when(config.get(HubbleOptions.PD_CLUSTER)).thenReturn(""); + Mockito.when(config.get(HubbleOptions.ROUTE_TYPE)).thenReturn(""); + Mockito.when(config.get(HubbleOptions.PD_PEERS)).thenReturn(""); + Mockito.when(config.get(HubbleOptions.SERVER_URL)) + .thenReturn("http://127.0.0.1:8080"); + return config; + } + + private void bindRequestSession() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + request.getSession().setAttribute(Constant.CREDENTIAL_PASSWORD_KEY, "pa"); + request.getSession().setAttribute(Constant.CREDENTIAL_EXPIRES_AT_KEY, + System.currentTimeMillis() + 10000L); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + } + + private void setField(Object object, String name, Object value) + throws Exception { + Class type = object.getClass(); + while (type != null) { + try { + Field field = type.getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + return; + } catch (NoSuchFieldException ignored) { + type = type.getSuperclass(); + } + } + throw new NoSuchFieldException(name); + } + + private static class TestIngestController extends IngestController { + + @Override + protected HugeClient authClient(String graphSpace, String graph) { + return Mockito.mock(HugeClient.class); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/langchain/LangChainControllerSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/langchain/LangChainControllerSecurityTest.java new file mode 100644 index 000000000..62abd1b82 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/langchain/LangChainControllerSecurityTest.java @@ -0,0 +1,269 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.langchain; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import org.apache.hugegraph.config.ConfigOption; +import org.apache.hugegraph.config.HugeConfig; + +public class LangChainControllerSecurityTest { + + @Test + public void testRequestPythonPathIsIgnored() throws Exception { + LangChainController controller = new LangChainController(); + String[] args = this.executeArgs(controller, "/tmp/evil-python", + "/tmp/script.py"); + + Assert.assertNotEquals("/tmp/evil-python", args[0]); + } + + @Test + public void testNoSchemaRejectsPathTraversalFileName() { + LangChainController controller = new LangChainController(); + LangChainController.RequestLangChainParams params = + LangChainController.RequestLangChainParams.builder() + .query("show vertices") + .model("gpt4") + .openKey("key") + .pythonPath("/bin/echo") + .fileName("../../pom.xml") + .build(); + + assertThrows(IllegalArgumentException.class, () -> { + controller.langchainNoSchema("DEFAULT", "graph", params); + }); + } + + @Test + public void testNoSchemaRejectsNonAllowlistedScript() throws Exception { + LangChainController controller = this.controllerWithProcessConfig(5); + LangChainController.RequestLangChainParams params = + LangChainController.RequestLangChainParams.builder() + .query("show vertices") + .model("gpt4") + .openKey("key") + .pythonPath("/bin/echo") + .fileName("other.py") + .build(); + + assertThrows(IllegalArgumentException.class, () -> { + controller.langchainNoSchema("DEFAULT", "graph", params); + }); + } + + @Test + public void testNoSchemaUsesConfiguredPythonForAllowedScript() + throws Exception { + LangChainController controller = this.controllerWithProcessConfig(5); + LangChainController.RequestLangChainParams params = + LangChainController.RequestLangChainParams.builder() + .query("show vertices") + .model("gpt4") + .openKey("key") + .pythonPath("/bin/echo") + .fileName("langchaincode/excute_langchain.py") + .build(); + + LangChainController.ResponseLangChain response = + (LangChainController.ResponseLangChain) controller.langchainNoSchema( + "DEFAULT", "graph", params); + + Assert.assertEquals("g.V()", response.getGremlin()); + } + + @Test + public void testProcessNonZeroExitFails() throws Exception { + LangChainController controller = this.controllerWithProcessConfig(5); + + assertThrows(IllegalStateException.class, () -> { + this.executeByProcessBuilder(controller, "/bin/sh", + this.resourcePath("langchaincode/fail.py")); + }); + } + + @Test + public void testProcessTimeoutFails() throws Exception { + LangChainController controller = this.controllerWithProcessConfig(1); + long start = System.nanoTime(); + + assertThrows(IllegalStateException.class, () -> { + this.executeByProcessBuilder(controller, "/bin/sh", + this.resourcePath("langchaincode/sleep.py")); + }); + + long elapsed = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start); + Assert.assertTrue(elapsed < 5L); + } + + @Test + public void testSecretValuesAreRedactedFromProcessOutput() + throws Exception { + LangChainController controller = new LangChainController(); + Set secrets = new HashSet<>(); + secrets.add("open-secret"); + secrets.add("ernie-secret"); + + String redacted = this.redact(controller, + "argv=open-secret err=ernie-secret", + secrets); + + Assert.assertEquals("argv=****** err=******", redacted); + } + + @Test + public void testNoGremlinFailureReturnsEmptyResult() + throws Exception { + LangChainController controller = this.controllerWithProcessConfig(5); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + PrintStream oldOut = System.out; + PrintStream oldErr = System.err; + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8)); + List result; + try { + result = this.executeByProcessBuilder( + controller, "/bin/sh", + this.resourcePath("langchaincode/secret_no_gremlin.py"), + "open-secret"); + } finally { + System.setOut(oldOut); + System.setErr(oldErr); + } + + Assert.assertTrue(result.isEmpty()); + String logs = output.toString(StandardCharsets.UTF_8); + if (!logs.isEmpty()) { + Assert.assertFalse(logs.contains("open-secret")); + Assert.assertTrue(logs.contains("******")); + } + } + + private String[] executeArgs(LangChainController controller, String python, + String script) throws Exception { + Method method = LangChainController.class.getDeclaredMethod( + "getExcuteArgs", String.class, String.class, + String.class, String.class, String.class, + String.class, String.class, String.class); + method.setAccessible(true); + return (String[]) method.invoke(controller, python, script, "q", "open", + "schema", "gpt4", null, null); + } + + private List executeByProcessBuilder(LangChainController controller, + String python, String script) + throws Exception { + return this.executeByProcessBuilder(controller, python, script, "open"); + } + + @SuppressWarnings("unchecked") + private List executeByProcessBuilder(LangChainController controller, + String python, String script, + String openKey) + throws Exception { + Method method = LangChainController.class.getDeclaredMethod( + "excutePythonByProcessBuilder", String.class, + String.class, String.class, String.class, String.class, + String.class, String.class, String.class); + method.setAccessible(true); + try { + return (List) method.invoke(controller, python, script, "q", + openKey, "schema", "gpt4", + null, null); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw e; + } + } + + private LangChainController controllerWithProcessConfig(int timeout) + throws Exception { + LangChainController controller = new LangChainController(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(Mockito.any())) + .thenAnswer(invocation -> { + ConfigOption option = invocation.getArgument(0); + if ("langchain.python_path".equals(option.name())) { + return "/bin/sh"; + } + if ("langchain.script_dir".equals(option.name())) { + return this.resourcePath("langchaincode"); + } + if ("langchain.script_allowlist".equals(option.name())) { + return Collections.singletonList("excute_langchain.py"); + } + if ("langchain.execute_timeout".equals(option.name())) { + return timeout; + } + return option.defaultValue(); + }); + Field field = LangChainController.class.getDeclaredField("config"); + field.setAccessible(true); + field.set(controller, config); + return controller; + } + + private String resourcePath(String name) { + return LangChainControllerSecurityTest.class.getClassLoader() + .getResource(name) + .getPath(); + } + + private String redact(LangChainController controller, String value, + Set secrets) throws Exception { + Method method = LangChainController.class.getDeclaredMethod( + "redact", String.class, Set.class); + method.setAccessible(true); + return (String) method.invoke(controller, value, secrets); + } + + private static void assertThrows(Class expected, + ThrowingRunnable runnable) { + try { + runnable.run(); + Assert.fail("Expected " + expected.getName()); + } catch (Throwable e) { + Assert.assertTrue("Expected " + expected.getName() + " but got " + + e.getClass().getName(), + expected.isInstance(e)); + } + } + + private interface ThrowingRunnable { + + void run() throws Throwable; + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/schema/SchemaControllerSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/schema/SchemaControllerSecurityTest.java new file mode 100644 index 000000000..9f8709c3b --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/schema/SchemaControllerSecurityTest.java @@ -0,0 +1,75 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.controller.schema; + +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletResponse; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.SchemaManager; + +public class SchemaControllerSecurityTest { + + @Test + public void testSchemaGroovyExportUsesSafeDownloadHeaders() + throws Exception { + String schema = "graph.schema().propertyKey('name').asText().create();"; + SchemaController controller = new TestSchemaController(schema); + MockHttpServletResponse response = new MockHttpServletResponse(); + + controller.schemaGroovyExport("DEFAULT\r\nX-Bad: injected", + "图 graph\";x", response); + + String header = response.getHeader("Content-Disposition"); + Assert.assertNotNull(header); + Assert.assertTrue(header.startsWith("attachment;")); + Assert.assertTrue(header.contains("filename=\"")); + Assert.assertTrue(header.contains("filename*=UTF-8''")); + Assert.assertFalse(header.contains("fileName=")); + Assert.assertFalse(header.contains("\r")); + Assert.assertFalse(header.contains("\n")); + Assert.assertFalse(header.contains("\";x")); + Assert.assertFalse(header.contains("图")); + Assert.assertEquals("application/octet-stream", + response.getContentType()); + Assert.assertEquals(schema, new String(response.getContentAsByteArray(), + StandardCharsets.UTF_8)); + } + + private static class TestSchemaController extends SchemaController { + + private final HugeClient client; + + TestSchemaController(String schema) { + SchemaManager schemaManager = Mockito.mock(SchemaManager.class); + Mockito.when(schemaManager.getGroovySchema()).thenReturn(schema); + this.client = Mockito.mock(HugeClient.class); + Mockito.when(this.client.schema()).thenReturn(schemaManager); + } + + @Override + protected HugeClient authClient(String graphSpace, String graph) { + return this.client; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ApiFieldNamingTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ApiFieldNamingTest.java new file mode 100644 index 000000000..eabac5a63 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ApiFieldNamingTest.java @@ -0,0 +1,81 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.util.Map; + +import org.junit.Test; +import org.mockito.Mockito; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.ExecuteHistory; +import org.apache.hugegraph.entity.schema.EdgeLabelEntity; +import org.apache.hugegraph.entity.schema.VertexLabelEntity; +import org.apache.hugegraph.service.graph.GraphService; +import org.apache.hugegraph.service.schema.EdgeLabelService; +import org.apache.hugegraph.service.schema.VertexLabelService; +import org.apache.hugegraph.testutil.Assert; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class ApiFieldNamingTest { + + @Test + public void testExecuteHistoryUsesGraphField() throws Exception { + ExecuteHistory history = new ExecuteHistory(); + history.setGraph("hugegraph"); + + String json = new ObjectMapper().writeValueAsString(history); + + Assert.assertTrue(json.contains("\"graph\":\"hugegraph\"")); + Assert.assertFalse(json.contains("\"graphe\"")); + } + + @Test + public void testGraphPropertiesUseLowerCamelCase() throws Exception { + GraphService service = new GraphService(); + VertexLabelService vertexLabels = Mockito.mock(VertexLabelService.class); + EdgeLabelService edgeLabels = Mockito.mock(EdgeLabelService.class); + VertexLabelEntity vertex = Mockito.mock(VertexLabelEntity.class); + EdgeLabelEntity edge = Mockito.mock(EdgeLabelEntity.class); + HugeClient client = Mockito.mock(HugeClient.class); + Mockito.when(vertexLabels.get("person", client)).thenReturn(vertex); + Mockito.when(edgeLabels.get("knows", client)).thenReturn(edge); + this.setField(service, "vlService", vertexLabels); + this.setField(service, "elService", edgeLabels); + + Map vertexFields = + service.getVertexProperties(client, "person"); + Map edgeFields = + service.getEdgeProperties(client, "knows"); + + Assert.assertTrue(vertexFields.containsKey("nullableProps")); + Assert.assertFalse(vertexFields.containsKey("NullableProps")); + Assert.assertTrue(edgeFields.containsKey("nullableProps")); + Assert.assertFalse(edgeFields.containsKey("NullableProps")); + } + + private void setField(Object target, String name, Object value) + throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AppTypeTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AppTypeTest.java new file mode 100644 index 000000000..60eebf283 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AppTypeTest.java @@ -0,0 +1,38 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import org.junit.Test; + +import org.apache.hugegraph.common.AppType; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.type.define.SerialEnum; + +public class AppTypeTest { + + @Test + public void testAppTypeRegistersItselfAsSerialEnum() { + Assert.assertEquals(AppType.GENERAL, + SerialEnum.fromCode(AppType.class, + AppType.GENERAL.code())); + Assert.assertEquals(AppType.CUSTOMIZED, + SerialEnum.fromCode(AppType.class, + AppType.CUSTOMIZED.code())); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java new file mode 100644 index 000000000..11f2b4c47 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java @@ -0,0 +1,875 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; + +import com.sun.net.httpserver.HttpServer; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.util.EntityUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Response; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.IngestionProxyServlet; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.controller.auth.LoginController; +import org.apache.hugegraph.driver.AuthManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.auth.UserEntity; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.exception.LoginThrottledException; +import org.apache.hugegraph.exception.ParameterizedException; +import org.apache.hugegraph.exception.ServerCapabilityUnavailableException; +import org.apache.hugegraph.exception.UnauthorizedException; +import org.apache.hugegraph.handler.CustomInterceptor; +import org.apache.hugegraph.handler.ExceptionAdvisor; +import org.apache.hugegraph.handler.LoginInterceptor; +import org.apache.hugegraph.handler.MessageSourceHandler; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.auth.LoginAttemptGuard; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.structure.auth.Login; +import org.apache.hugegraph.structure.auth.LoginResult; + +public class AuthSecurityTest { + + @After + public void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void testLoginInterceptorRejectsMissingSessionAuth() { + LoginInterceptor interceptor = new LoginInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/api/v1.3/auth/status"); + + assertThrows(UnauthorizedException.class, () -> { + interceptor.preHandle(request, new MockHttpServletResponse(), null); + }); + } + + @Test + public void testLoginInterceptorRequiresTokenAndUsername() { + LoginInterceptor interceptor = new LoginInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/api/v1.3/auth/status"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + + assertThrows(UnauthorizedException.class, () -> { + interceptor.preHandle(request, new MockHttpServletResponse(), null); + }); + + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + } + + @Test + public void testLoginInterceptorRejectsBlankSessionAuth() { + LoginInterceptor interceptor = new LoginInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/api/v1.3/auth/status"); + request.getSession().setAttribute(Constant.TOKEN_KEY, " "); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + + assertThrows(UnauthorizedException.class, () -> { + interceptor.preHandle(request, new MockHttpServletResponse(), null); + }); + + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, " "); + + assertThrows(UnauthorizedException.class, () -> { + interceptor.preHandle(request, new MockHttpServletResponse(), null); + }); + } + + @Test + public void testLoginInterceptorAllowsOptionsPreflight() { + LoginInterceptor interceptor = new LoginInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "OPTIONS", "/api/v1.3/auth/status"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + } + + @Test + public void testCustomInterceptorDoesNotCreateClientForMissingSession() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/api/v1.3/auth/status"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testCustomInterceptorDoesNotCreateClientForPartialSession() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/api/v1.3/auth/status"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testCustomInterceptorDoesNotCreateClientWithoutToken() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/api/v1.3/auth/status"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testCustomInterceptorDoesNotCreateClientForBlankSession() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/api/v1.3/auth/status"); + request.getSession().setAttribute(Constant.TOKEN_KEY, " "); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, " "); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testCustomInterceptorDoesNotCreateClientForOptions() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "OPTIONS", + "/api/v1.3/graphspaces/space1"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testCustomInterceptorKeepsUnauthClientForLogin() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "POST", "/api/v1.3/auth/login"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(1, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testCustomInterceptorCreatesClientForAuthenticatedApi() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", + "/api/v1.3/graphspaces/space1" + + "/graphs/graph1/schema"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(1, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertEquals("space1", interceptor.graphSpace); + Assert.assertEquals("graph1", interceptor.graph); + Assert.assertEquals("token", interceptor.token); + } + + @Test + public void testCustomInterceptorDoesNotCreateClientForLogout() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "POST", "/api/v1.3/auth/logout"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(0, interceptor.authClients); + Assert.assertEquals(0, interceptor.unauthClients); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testUnauthorizedExceptionUsesHttp401() throws Exception { + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(new MockHttpServletRequest())); + ExceptionAdvisor advisor = new ExceptionAdvisor(); + Response response = advisor.exceptionHandler(new UnauthorizedException()); + + Assert.assertEquals(Constant.STATUS_UNAUTHORIZED, response.getStatus()); + + Method method = ExceptionAdvisor.class.getMethod("exceptionHandler", + UnauthorizedException.class); + ResponseStatus status = method.getAnnotation(ResponseStatus.class); + Assert.assertEquals(HttpStatus.UNAUTHORIZED, status.value()); + } + + @Test + public void testLoginThrottleUsesHttp429AndRetryAfter() { + ExceptionAdvisor advisor = new ExceptionAdvisor(); + MessageSourceHandler messageSource = + Mockito.mock(MessageSourceHandler.class); + Mockito.when(messageSource.getMessage(Mockito.anyString(), + Mockito.any())) + .thenReturn("retry later"); + ReflectionTestUtils.setField(advisor, "messageSourceHandler", + messageSource); + + ResponseEntity response = advisor.exceptionHandler( + new LoginThrottledException(5L)); + + Assert.assertEquals(HttpStatus.TOO_MANY_REQUESTS, + response.getStatusCode()); + Assert.assertEquals("5", response.getHeaders().getFirst("Retry-After")); + Assert.assertEquals(HttpStatus.TOO_MANY_REQUESTS.value(), + response.getBody().getStatus()); + } + + @Test + public void testMissingRequestParameterUsesActionableHttp400() + throws Exception { + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(new MockHttpServletRequest())); + ExceptionAdvisor advisor = new ExceptionAdvisor(); + MessageSourceHandler messageSource = + Mockito.mock(MessageSourceHandler.class); + Mockito.when(messageSource.getMessage(Mockito.anyString(), + Mockito.any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + ReflectionTestUtils.setField(advisor, "messageSourceHandler", + messageSource); + Method method = ExceptionAdvisor.class.getMethod( + "exceptionHandler", + MissingServletRequestParameterException.class); + ResponseStatus status = method.getAnnotation(ResponseStatus.class); + + Response response = (Response) method.invoke( + advisor, + new MissingServletRequestParameterException("type", "int")); + + Assert.assertEquals(HttpStatus.BAD_REQUEST, status.value()); + Assert.assertEquals(Constant.STATUS_BAD_REQUEST, response.getStatus()); + Assert.assertEquals("request.parameter.required", response.getMessage()); + Assert.assertNull(response.getCause()); + } + + @Test + public void testExceptionResponsesDoNotExposeInternalCause() { + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(new MockHttpServletRequest())); + ExceptionAdvisor advisor = new ExceptionAdvisor(); + MessageSourceHandler messageSource = + Mockito.mock(MessageSourceHandler.class); + Mockito.when(messageSource.getMessage(Mockito.anyString(), + Mockito.any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + ReflectionTestUtils.setField(advisor, "messageSourceHandler", + messageSource); + RuntimeException cause = new RuntimeException("internal-secret"); + + Assert.assertNull(advisor.exceptionHandler( + new InternalException("internal", cause)).getCause()); + Assert.assertNull(advisor.exceptionHandler( + new ExternalException("external", cause)).getCause()); + Assert.assertNull(advisor.exceptionHandler( + new ParameterizedException("parameter", cause)).getCause()); + Assert.assertNull(advisor.exceptionHandler((Exception) cause).getCause()); + } + + @Test + public void testMissingServerCapabilityUsesHttp503WithoutCause() + throws Exception { + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(new MockHttpServletRequest())); + ExceptionAdvisor advisor = new ExceptionAdvisor(); + MessageSourceHandler messageSource = + Mockito.mock(MessageSourceHandler.class); + Mockito.when(messageSource.getMessage(Mockito.anyString(), + Mockito.any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + ReflectionTestUtils.setField(advisor, "messageSourceHandler", + messageSource); + Method method = ExceptionAdvisor.class.getMethod( + "exceptionHandler", + ServerCapabilityUnavailableException.class); + ResponseStatus status = method.getAnnotation(ResponseStatus.class); + + Response response = (Response) method.invoke( + advisor, + new ServerCapabilityUnavailableException( + "server.capability.pd-status.unavailable", + new RuntimeException("downstream details"))); + + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, status.value()); + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE.value(), + response.getStatus()); + Assert.assertEquals("server.capability.pd-status.unavailable", + response.getMessage()); + Assert.assertNull(response.getCause()); + } + + @Test + public void testIngestionProxyRejectsMissingSessionWithHttp401() + throws Exception { + TestIngestionProxyServlet servlet = new TestIngestionProxyServlet(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/ingest/tasks"); + + HttpResponse response = servlet.execute(request); + + Assert.assertEquals(Constant.STATUS_UNAUTHORIZED, + response.getStatusLine().getStatusCode()); + Assert.assertEquals("{\"status\": 401}", + EntityUtils.toString(response.getEntity())); + } + + @Test + public void testIngestionProxyRejectsUsernameWithoutToken() + throws Exception { + TestIngestionProxyServlet servlet = new TestIngestionProxyServlet(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", "/ingest/tasks"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + + HttpResponse response = servlet.execute(request); + + Assert.assertEquals(Constant.STATUS_UNAUTHORIZED, + response.getStatusLine().getStatusCode()); + Assert.assertEquals("{\"status\": 401}", + EntityUtils.toString(response.getEntity())); + } + + @Test + public void testCredentialPasswordIsShortLivedAndNotLegacySessionKey() { + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + TestBaseController controller = new TestBaseController(); + + controller.savePassword("pa"); + + Assert.assertEquals("pa", controller.readPassword()); + Assert.assertNull(request.getSession().getAttribute("password")); + + request.getSession().setAttribute(Constant.CREDENTIAL_EXPIRES_AT_KEY, + System.currentTimeMillis() - 1L); + Assert.assertNull(controller.readPassword()); + Assert.assertNull(request.getSession().getAttribute( + Constant.CREDENTIAL_PASSWORD_KEY)); + } + + @Test + public void testClearAuthSessionClearsCredentialState() { + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + TestBaseController controller = new TestBaseController(); + + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + controller.savePassword("pa"); + + controller.clearAuth(); + + Assert.assertNull(request.getSession().getAttribute(Constant.TOKEN_KEY)); + Assert.assertNull(request.getSession().getAttribute(Constant.USERNAME_KEY)); + Assert.assertNull(request.getSession().getAttribute( + Constant.CREDENTIAL_PASSWORD_KEY)); + Assert.assertNull(request.getSession().getAttribute( + Constant.CREDENTIAL_EXPIRES_AT_KEY)); + } + + @Test + public void testStandaloneLoginUsesServerPr3008Payload() + throws Exception { + AtomicReference bodyRef = new AtomicReference<>(); + AtomicReference authRef = new AtomicReference<>(); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", + 0), + 0); + server.createContext("/auth/login", exchange -> { + authRef.set(exchange.getRequestHeaders() + .getFirst("Authorization")); + bodyRef.set(new String(exchange.getRequestBody().readAllBytes(), + StandardCharsets.UTF_8)); + byte[] body = "{\"token\":\"server-token\"}".getBytes( + StandardCharsets.UTF_8); + exchange.sendResponseHeaders(Constant.STATUS_OK, body.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(body); + } + }); + server.start(); + try { + TestLoginController controller = new TestLoginController(); + controller.useNetwork = true; + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.SERVER_URL)) + .thenReturn("http://127.0.0.1:" + + server.getAddress().getPort()); + setField(controller, "config", config); + Login login = new Login(); + login.name("admin"); + login.password("pa"); + + LoginResult result = controller.standalone(login); + + Assert.assertEquals("server-token", result.token()); + Assert.assertTrue(authRef.get().startsWith("Basic ")); + Assert.assertTrue(bodyRef.get().contains("\"user_name\":\"admin\"")); + Assert.assertTrue(bodyRef.get().contains("\"user_password\":\"pa\"")); + Assert.assertTrue(bodyRef.get().contains("\"token_expire\":")); + } finally { + server.stop(0); + } + } + + @Test + public void testLoginCommitsAuthAndRotatesSessionAfterValidation() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + String oldSessionId = request.getSession().getId(); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + TestLoginController controller = new TestLoginController(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(false); + setField(controller, "config", config); + Login login = new Login(); + login.name("admin"); + login.password("pa"); + + controller.login(login); + + Assert.assertNotEquals(oldSessionId, request.getSession().getId()); + Assert.assertEquals("admin", request.getSession().getAttribute( + Constant.USERNAME_KEY)); + Assert.assertEquals("server-token", request.getSession().getAttribute( + Constant.TOKEN_KEY)); + } + + @Test + public void testPdLoginValidatesUserWithFreshLoginToken() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession().setAttribute(Constant.TOKEN_KEY, "old-token"); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + TestLoginController controller = new TestLoginController(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true); + setField(controller, "config", config); + + LoginResult loginResult = new LoginResult(); + loginResult.token("server-token"); + AuthManager auth = Mockito.mock(AuthManager.class); + Mockito.when(auth.login(Mockito.any())).thenReturn(loginResult); + HugeClient loginClient = Mockito.mock(HugeClient.class); + Mockito.when(loginClient.auth()).thenReturn(auth); + controller.loginClient = loginClient; + + HugeClient userClient = Mockito.mock(HugeClient.class); + controller.userClient = userClient; + UserService users = Mockito.mock(UserService.class); + UserEntity entity = new UserEntity(); + Mockito.when(users.getUser(userClient, "admin")).thenReturn(entity); + ReflectionTestUtils.setField(controller, "userService", users); + Login login = new Login(); + login.name("admin"); + login.password("pa"); + + controller.login(login); + + Assert.assertEquals("server-token", controller.validationToken); + Mockito.verify(userClient).close(); + Assert.assertEquals("server-token", request.getSession().getAttribute( + Constant.TOKEN_KEY)); + } + + @Test + public void testLoginFailureClearsExistingAuthentication() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession().setAttribute(Constant.USERNAME_KEY, "old-user"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "old-token"); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + TestLoginController controller = new TestLoginController(); + controller.failStandalone = true; + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(false); + setField(controller, "config", config); + Login login = new Login(); + login.name("admin"); + login.password("pa"); + + try { + controller.login(login); + Assert.fail("Expected login failure"); + } catch (RuntimeException ignored) { + // Expected. + } + + Assert.assertNull(request.getSession().getAttribute( + Constant.USERNAME_KEY)); + Assert.assertNull(request.getSession().getAttribute(Constant.TOKEN_KEY)); + Mockito.verify(controller.attemptGuard) + .recordFailure("admin", "127.0.0.1"); + } + + @Test + public void testPdLoginClosesClientsWhenUserValidationFails() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + TestLoginController controller = new TestLoginController(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true); + setField(controller, "config", config); + + LoginResult loginResult = new LoginResult(); + loginResult.token("server-token"); + AuthManager auth = Mockito.mock(AuthManager.class); + Mockito.when(auth.login(Mockito.any())).thenReturn(loginResult); + HugeClient loginClient = Mockito.mock(HugeClient.class); + Mockito.when(loginClient.auth()).thenReturn(auth); + HugeClient userClient = Mockito.mock(HugeClient.class); + controller.loginClient = loginClient; + controller.userClient = userClient; + UserService users = Mockito.mock(UserService.class); + Mockito.when(users.getUser(userClient, "admin")) + .thenThrow(new ExternalException(HttpStatus.UNAUTHORIZED.value(), + "expected validation failure")); + ReflectionTestUtils.setField(controller, "userService", users); + Login login = new Login(); + login.name("admin"); + login.password("pa"); + + try { + controller.login(login); + Assert.fail("Expected user validation failure"); + } catch (RuntimeException ignored) { + // Expected. + } + + Mockito.verify(loginClient).close(); + Mockito.verify(userClient).close(); + Mockito.verify(controller.attemptGuard, Mockito.never()) + .recordFailure(Mockito.anyString(), Mockito.anyString()); + Assert.assertNull(request.getSession().getAttribute(Constant.TOKEN_KEY)); + } + + @Test + public void testStandaloneLoginConfiguresTimeoutsAndDisconnects() + throws Exception { + TestLoginController controller = new TestLoginController(); + controller.connection = new StubHttpURLConnection( + new URL("http://unused/auth/login")); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.SERVER_URL)) + .thenReturn("http://unused"); + setField(controller, "config", config); + Login login = new Login(); + login.name("admin"); + login.password("pa"); + + controller.standalone(login); + + Assert.assertTrue(controller.connection.getConnectTimeout() > 0); + Assert.assertTrue(controller.connection.getReadTimeout() > 0); + Assert.assertTrue(controller.connection.disconnected); + } + + private static class TestIngestionProxyServlet + extends IngestionProxyServlet { + + public HttpResponse execute(MockHttpServletRequest request) + throws IOException { + return this.doExecute(request, new MockHttpServletResponse(), + new HttpGet("/ingest/tasks")); + } + } + + private static class TestBaseController extends BaseController { + + public void savePassword(String password) { + this.setCredentialPassword(password); + } + + public String readPassword() { + return this.getCredentialPassword(); + } + + public void clearAuth() { + this.clearAuthSession(); + } + } + + private static class TestLoginController extends LoginController { + + private boolean failStandalone; + private boolean useNetwork; + private StubHttpURLConnection connection; + private HugeClient loginClient; + private HugeClient userClient; + private String validationToken; + private LoginAttemptGuard attemptGuard; + + private TestLoginController() { + this.attemptGuard = Mockito.mock(LoginAttemptGuard.class); + ReflectionTestUtils.setField(this, "loginAttemptGuard", + this.attemptGuard); + } + + public LoginResult standalone(Login login) { + return this.loginStandalone(login); + } + + @Override + protected LoginResult loginStandalone(Login login) { + if (this.failStandalone) { + throw new ExternalException(HttpStatus.UNAUTHORIZED.value(), + "expected login failure"); + } + if (this.connection != null || this.useNetwork) { + return super.loginStandalone(login); + } + LoginResult result = new LoginResult(); + result.token("server-token"); + return result; + } + + @Override + protected HttpURLConnection openConnection(URL endpoint) + throws IOException { + if (this.useNetwork) { + return super.openConnection(endpoint); + } + if (this.connection == null) { + this.connection = new StubHttpURLConnection(endpoint); + } + return this.connection; + } + + @Override + protected HugeClient createLoginClient(String username, + String password) { + return this.loginClient != null ? this.loginClient : + super.createLoginClient(username, password); + } + + @Override + protected HugeClient createLoginTokenClient(String token) { + if (this.userClient == null) { + return super.createLoginTokenClient(token); + } + this.validationToken = token; + return this.userClient; + } + } + + private static class StubHttpURLConnection extends HttpURLConnection { + + private final ByteArrayOutputStream request = new ByteArrayOutputStream(); + private boolean disconnected; + + protected StubHttpURLConnection(URL url) { + super(url); + } + + @Override + public void disconnect() { + this.disconnected = true; + } + + @Override + public boolean usingProxy() { + return false; + } + + @Override + public void connect() { + // No-op test connection. + } + + @Override + public OutputStream getOutputStream() { + return this.request; + } + + @Override + public int getResponseCode() { + return Constant.STATUS_OK; + } + + @Override + public ByteArrayInputStream getInputStream() { + return new ByteArrayInputStream( + "{\"token\":\"server-token\"}".getBytes( + StandardCharsets.UTF_8)); + } + } + + private static class TestCustomInterceptor extends CustomInterceptor { + + private int authClients; + private int unauthClients; + private String graphSpace; + private String graph; + private String token; + + @Override + protected org.apache.hugegraph.driver.HugeClient authClient( + String graphSpace, String graph, String token) { + this.authClients++; + this.graphSpace = graphSpace; + this.graph = graph; + this.token = token; + return null; + } + + @Override + protected org.apache.hugegraph.driver.HugeClient unauthClient() { + this.unauthClients++; + return null; + } + } + + private static void setField(Object object, String name, Object value) + throws Exception { + Class type = object.getClass(); + while (type != null) { + try { + Field field = type.getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + return; + } catch (NoSuchFieldException ignored) { + type = type.getSuperclass(); + } + } + throw new NoSuchFieldException(name); + } + + private static void assertThrows(Class expected, + ThrowingRunnable runnable) { + try { + runnable.run(); + } catch (Throwable actual) { + if (expected.isInstance(actual)) { + return; + } + throw new AssertionError("Unexpected exception type", actual); + } + throw new AssertionError("Expected exception: " + expected.getName()); + } + + private interface ThrowingRunnable { + + void run() throws Throwable; + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthzRouteRegistrationTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthzRouteRegistrationTest.java new file mode 100644 index 000000000..e62b86fc5 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthzRouteRegistrationTest.java @@ -0,0 +1,188 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.Collections; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.hugegraph.entity.auth.AccessEntity; +import org.apache.hugegraph.service.auth.AccessService; +import org.apache.hugegraph.service.auth.RoleService; +import org.apache.hugegraph.structure.auth.Access; +import org.apache.hugegraph.structure.auth.Group; +import org.apache.hugegraph.structure.auth.HugePermission; +import org.apache.hugegraph.structure.auth.Role; +import org.apache.hugegraph.structure.auth.Target; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +public class AuthzRouteRegistrationTest { + + private static final String API = "/api/v1.3/"; + + @Test + public void testGraphspaceAuthzControllersAreRegistered() throws Exception { + assertController("org.apache.hugegraph.controller.auth.TargetController", + "graphspaces/{graphspace}/auth/targets"); + assertController("org.apache.hugegraph.controller.auth.RoleController", + "graphspaces/{graphspace}/auth/roles"); + assertController("org.apache.hugegraph.controller.auth.AccessController", + "graphspaces/{graphspace}/auth/accesses"); + assertController("org.apache.hugegraph.controller.auth.BelongController", + "graphspaces/{graphspace}/auth/belongs"); + assertController("org.apache.hugegraph.controller.auth.GraphSpaceUserController", + "graphspaces/{graphspace}/auth/users"); + } + + @Test + public void testGraphspaceAuthzServicesAreRegistered() throws Exception { + assertService("org.apache.hugegraph.service.auth.TargetService"); + assertService("org.apache.hugegraph.service.auth.RoleService"); + assertService("org.apache.hugegraph.service.auth.AccessService"); + assertService("org.apache.hugegraph.service.auth.BelongService"); + assertService("org.apache.hugegraph.service.auth.GraphSpaceUserService"); + } + + @Test + public void testRoleDtoCarriesPathGraphspace() { + Group group = new Group(); + group.setId("-38:slice5-role"); + group.name("slice5-role"); + + Role role = TestRoleService.toRole("DEFAULT", group); + + Assert.assertEquals("DEFAULT", role.graphSpace()); + Assert.assertEquals(group.id(), role.id()); + Assert.assertEquals("slice5-role", role.name()); + } + + @Test + public void testAccessDtoCarriesPathGraphspace() throws Exception { + Group group = new Group(); + group.setId("-38:slice5-role"); + group.name("slice5-role"); + + Target target = new Target(); + target.setId("-46:slice5-target"); + target.name("slice5-target"); + target.graph("hugegraph"); + target.graphSpace(null); + target.description("target desc"); + target.resources(Collections.singletonList( + ImmutableMap.of("type", "GREMLIN", + "label", "*"))); + + Access access = new Access(); + access.group(group); + access.target(target); + access.permission(HugePermission.READ); + + AccessEntity entity = new TestAccessService() + .exposeConvert("DEFAULT", access, group, target); + + Assert.assertEquals("DEFAULT", entity.getGraphSpace()); + Assert.assertEquals("hugegraph", entity.getGraph()); + Assert.assertEquals(target.id(), entity.getTargetId()); + Assert.assertEquals(group.id(), entity.getRoleId()); + Assert.assertEquals(Collections.singleton(HugePermission.READ), + entity.getPermissions()); + Assert.assertEquals(target.resourcesList(), entity.getResources()); + } + + @Test + public void testAccessListDtoCarriesPathGraphspace() throws Exception { + Group group = new Group(); + group.setId("-38:slice5-role"); + group.name("slice5-role"); + + Target target = new Target(); + target.setId("-46:slice5-target"); + target.name("slice5-target"); + target.graph("hugegraph"); + target.graphSpace(null); + target.resources(Collections.singletonList( + ImmutableMap.of("type", "GREMLIN", + "label", "*"))); + + Access read = new Access(); + read.group(group); + read.target(target); + read.permission(HugePermission.READ); + + Access write = new Access(); + write.group(group); + write.target(target); + write.permission(HugePermission.WRITE); + + AccessEntity entity = new TestAccessService() + .exposeConvert("DEFAULT", ImmutableList.of(read, write), + group, target); + + Assert.assertEquals("DEFAULT", entity.getGraphSpace()); + Assert.assertEquals(target.id(), entity.getTargetId()); + Assert.assertEquals(group.id(), entity.getRoleId()); + Assert.assertEquals(ImmutableSet.of(HugePermission.READ, + HugePermission.WRITE), + entity.getPermissions()); + } + + private static void assertController(String className, String path) + throws Exception { + Class type = Class.forName(className); + + Assert.assertNotNull(type.getAnnotation(RestController.class)); + RequestMapping mapping = type.getAnnotation(RequestMapping.class); + Assert.assertNotNull(mapping); + Assert.assertEquals(API + path, mapping.value()[0]); + } + + private static void assertService(String className) throws Exception { + Class type = Class.forName(className); + + Assert.assertNotNull(type.getAnnotation(Service.class)); + } + + private static class TestRoleService extends RoleService { + + public static Role toRole(String graphSpace, Group group) { + return RoleService.toRole(graphSpace, group); + } + } + + private static class TestAccessService extends AccessService { + + public AccessEntity exposeConvert(String graphSpace, Access access, + Group group, Target target) { + return this.convert(graphSpace, access, group, target); + } + + public AccessEntity exposeConvert(String graphSpace, + Iterable accesses, + Group group, Target target) { + return this.convert(graphSpace, + ImmutableList.copyOf(accesses), group, + target); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java new file mode 100644 index 000000000..0320847c0 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java @@ -0,0 +1,136 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.driver.HugeClient; + +public class BaseControllerGremlinClientTest { + + @After + public void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void testGremlinClientUsesShortLivedBasicCredential() { + HugeClient tokenClient = Mockito.mock(HugeClient.class); + HugeClient basicClient = Mockito.mock(HugeClient.class); + + MockHttpServletRequest request = this.requestWithAuth(); + request.setAttribute("hugeClient", tokenClient); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + + TestController controller = new TestController(); + controller.basicClient = basicClient; + + HugeClient client = controller.gremlinClient("DEFAULT", "hugegraph"); + + Assert.assertSame(basicClient, client); + Assert.assertSame(basicClient, request.getAttribute("hugeClient")); + Assert.assertTrue(controller.basicClientCreated); + Assert.assertFalse(controller.authClientCreated); + Assert.assertEquals("DEFAULT", controller.graphSpace); + Assert.assertEquals("hugegraph", controller.graph); + Assert.assertEquals("admin", controller.username); + Assert.assertEquals("pa", controller.password); + Mockito.verify(tokenClient).close(); + } + + @Test + public void testGremlinClientFallsBackToAuthClientWithoutCredential() { + HugeClient tokenClient = Mockito.mock(HugeClient.class); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "jwt"); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + + TestController controller = new TestController(); + controller.authClient = tokenClient; + + HugeClient client = controller.gremlinClient("DEFAULT", "hugegraph"); + + Assert.assertSame(tokenClient, client); + Assert.assertSame(tokenClient, request.getAttribute("hugeClient")); + Assert.assertFalse(controller.basicClientCreated); + Assert.assertTrue(controller.authClientCreated); + Assert.assertEquals("DEFAULT", controller.graphSpace); + Assert.assertEquals("hugegraph", controller.graph); + } + + private MockHttpServletRequest requestWithAuth() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "jwt"); + request.getSession().setAttribute(Constant.CREDENTIAL_PASSWORD_KEY, "pa"); + request.getSession().setAttribute( + Constant.CREDENTIAL_EXPIRES_AT_KEY, + System.currentTimeMillis() + Constant.CREDENTIAL_TTL_MILLIS); + return request; + } + + private static class TestController extends BaseController { + + private HugeClient basicClient; + private HugeClient authClient; + private boolean basicClientCreated; + private boolean authClientCreated; + private String graphSpace; + private String graph; + private String username; + private String password; + + HugeClient gremlinClient(String graphSpace, String graph) { + return this.authGremlinClient(graphSpace, graph); + } + + @Override + protected HugeClient createBasicClient(String graphSpace, String graph, + String username, + String password) { + this.basicClientCreated = true; + this.graphSpace = graphSpace; + this.graph = graph; + this.username = username; + this.password = password; + return this.basicClient; + } + + @Override + protected HugeClient authClient(String graphSpace, String graph) { + this.authClientCreated = true; + this.graphSpace = graphSpace; + this.graph = graph; + this.getRequest().setAttribute("hugeClient", this.authClient); + return this.authClient; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BusinessAssertTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BusinessAssertTest.java new file mode 100644 index 000000000..eb55185fe --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BusinessAssertTest.java @@ -0,0 +1,97 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.junit.Test; + +import org.apache.hugegraph.testutil.Assert; + +public class BusinessAssertTest { + + private static final Pattern BUSINESS_ASSERT = Pattern.compile( + "^\\s*assert\\b", Pattern.MULTILINE); + + @Test + public void testHubbleBusinessSourcesDoNotUseJavaAssert() + throws Exception { + Path repo = repoRoot(); + List roots = Arrays.asList( + repo.resolve("hugegraph-hubble/hubble-be/src/main/java/" + + "org/apache/hugegraph/controller"), + repo.resolve("hugegraph-hubble/hubble-be/src/main/java/" + + "org/apache/hugegraph/service") + ); + List violations = new ArrayList<>(); + + for (Path root : roots) { + collectAsserts(root, violations); + } + + Assert.assertTrue(violations.toString(), violations.isEmpty()); + } + + private static Path repoRoot() { + Path current = Paths.get("").toAbsolutePath(); + while (current != null) { + if (Files.isDirectory(current.resolve("hugegraph-client")) && + Files.isDirectory(current.resolve("hugegraph-hubble"))) { + return current; + } + current = current.getParent(); + } + throw new IllegalStateException("Failed to locate repository root"); + } + + private static void collectAsserts(Path root, List violations) + throws IOException { + if (!Files.isDirectory(root)) { + return; + } + + try (Stream files = Files.walk(root)) { + files.filter(path -> path.toString().endsWith(".java")) + .filter(path -> !path.toString().contains("/target/")) + .forEach(path -> collectAssertsInFile(path, violations)); + } + } + + private static void collectAssertsInFile(Path path, + List violations) { + try { + String content = new String(Files.readAllBytes(path), + StandardCharsets.UTF_8); + if (BUSINESS_ASSERT.matcher(content).find()) { + violations.add(path.toString()); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to read " + path, e); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConsolePrintTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConsolePrintTest.java new file mode 100644 index 000000000..9283cd085 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConsolePrintTest.java @@ -0,0 +1,65 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.Test; + +import org.apache.hugegraph.testutil.Assert; + +public class ConsolePrintTest { + + @Test + public void testMainSourceDoesNotUseConsolePrints() throws Exception { + Path sourceRoot = Paths.get("src/main/java"); + List violations = new ArrayList<>(); + + try (Stream files = Files.walk(sourceRoot)) { + files.filter(path -> path.toString().endsWith(".java")) + .forEach(path -> collectConsolePrints(path, violations)); + } + + Assert.assertTrue(violations.toString(), violations.isEmpty()); + } + + private static void collectConsolePrints(Path path, + List violations) { + try { + List lines = Files.readAllLines(path, + StandardCharsets.UTF_8); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + if (line.contains("System.out.println") || + line.contains("System.err.println") || + line.contains(".printStackTrace()")) { + violations.add(path + ":" + (i + 1)); + } + } + } catch (Exception e) { + throw new IllegalStateException("Failed to read " + path, e); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/CypherHistoryFailureTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/CypherHistoryFailureTest.java new file mode 100644 index 000000000..933ce8acf --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/CypherHistoryFailureTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import org.apache.hugegraph.controller.query.CypherController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.ExecuteHistory; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class CypherHistoryFailureTest { + + @Test + public void testSyncFailurePersistsOnlyControlledReasonCode() { + HugeClient client = Mockito.mock(HugeClient.class); + QueryService queryService = Mockito.mock(QueryService.class); + ExecuteHistoryService historyService = + Mockito.mock(ExecuteHistoryService.class); + RuntimeException unsafe = new RuntimeException( + "No signature of method: secret.Groovy.stack()"); + Mockito.when(queryService.executeCypherQuery( + Mockito.eq(client), Mockito.anyString())) + .thenThrow(unsafe); + + TestController controller = new TestController(client); + ReflectionTestUtils.setField(controller, "queryService", queryService); + ReflectionTestUtils.setField(controller, "historyService", historyService); + + Assert.assertThrows(RuntimeException.class, + () -> controller.execute("DEFAULT", "hugegraph", + "MATCH INVALID")); + + ArgumentCaptor saved = + ArgumentCaptor.forClass(ExecuteHistory.class); + Mockito.verify(historyService).update(saved.capture()); + Assert.assertEquals("GREMLIN_EXECUTION_FAILED", + saved.getValue().getFailureReason()); + Assert.assertFalse(saved.getValue().getFailureReason() + .contains("signature")); + } + + @Test + public void testAsyncSubmissionFailureDoesNotClaimExecutionFailure() { + HugeClient client = Mockito.mock(HugeClient.class); + QueryService queryService = Mockito.mock(QueryService.class); + ExecuteHistoryService historyService = + Mockito.mock(ExecuteHistoryService.class); + Mockito.when(queryService.executeCypherAsyncTask( + Mockito.eq(client), Mockito.anyString())) + .thenThrow(new RuntimeException("secret backend detail")); + + TestController controller = new TestController(client); + ReflectionTestUtils.setField(controller, "queryService", queryService); + ReflectionTestUtils.setField(controller, "historyService", historyService); + + Assert.assertThrows(RuntimeException.class, + () -> controller.executeAsyncTask( + "DEFAULT", "hugegraph", + new GremlinQuery("MATCH INVALID"))); + + ArgumentCaptor saved = + ArgumentCaptor.forClass(ExecuteHistory.class); + Mockito.verify(historyService).update(saved.capture()); + Assert.assertNull(saved.getValue().getFailureReason()); + } + + private static class TestController extends CypherController { + + private final HugeClient client; + + TestController(HugeClient client) { + this.client = client; + } + + @Override + protected HugeClient authClient(String graphSpace, String graph) { + return this.client; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/DashboardControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/DashboardControllerTest.java new file mode 100644 index 000000000..bd8cbc171 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/DashboardControllerTest.java @@ -0,0 +1,52 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.Map; + +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.op.DashboardController; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.testutil.Assert; + +public class DashboardControllerTest { + + @Test + public void testReturnsConfiguredProtocolAndHostPortWithoutProbing() { + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.DASHBOARD_ADDRESS)) + .thenReturn("127.0.0.1:8092"); + Mockito.when(config.get(HubbleOptions.SERVER_PROTOCOL)) + .thenReturn("https"); + DashboardController controller = new DashboardController(); + ReflectionTestUtils.setField(controller, "config", config); + + Map result = controller.listOperations(); + + Assert.assertEquals("127.0.0.1:8092", result.get("address")); + Assert.assertEquals("https", result.get("protocol")); + Mockito.verify(config).get(HubbleOptions.DASHBOARD_ADDRESS); + Mockito.verify(config).get(HubbleOptions.SERVER_PROTOCOL); + Mockito.verifyNoMoreInteractions(config); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/EmptyCatchTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/EmptyCatchTest.java new file mode 100644 index 000000000..09a4e07ad --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/EmptyCatchTest.java @@ -0,0 +1,100 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.junit.Test; + +import org.apache.hugegraph.testutil.Assert; + +public class EmptyCatchTest { + + private static final Pattern EMPTY_CATCH = Pattern.compile( + "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}", + Pattern.MULTILINE | Pattern.DOTALL); + + @Test + public void testJavaSourcesDoNotContainEmptyCatchBlocks() + throws Exception { + Path repo = repoRoot(); + List roots = Arrays.asList( + repo.resolve("hugegraph-client/src"), + repo.resolve("hugegraph-loader/src"), + repo.resolve("hugegraph-tools/src"), + repo.resolve("hugegraph-spark-connector/src"), + repo.resolve("hugegraph-hubble/hubble-be/src") + ); + List violations = new ArrayList<>(); + + for (Path root : roots) { + collectEmptyCatches(root, violations); + } + + Assert.assertTrue(violations.toString(), violations.isEmpty()); + } + + private static Path repoRoot() { + Path current = Paths.get("").toAbsolutePath(); + while (current != null) { + if (Files.isDirectory(current.resolve("hugegraph-client")) && + Files.isDirectory(current.resolve("hugegraph-hubble"))) { + return current; + } + current = current.getParent(); + } + throw new IllegalStateException("Failed to locate repository root"); + } + + private static void collectEmptyCatches(Path root, + List violations) + throws IOException { + if (!Files.isDirectory(root)) { + return; + } + + try (Stream files = Files.walk(root)) { + files.filter(path -> path.toString().endsWith(".java")) + .filter(path -> !path.toString().contains("/target/")) + .forEach(path -> collectEmptyCatchesInFile(path, violations)); + } + } + + private static void collectEmptyCatchesInFile(Path path, + List violations) { + try { + String content = new String(Files.readAllBytes(path), + StandardCharsets.UTF_8); + if (EMPTY_CATCH.matcher(content).find()) { + violations.add(path.toString()); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to read " + path, e); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/EntityUtilTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/EntityUtilTest.java deleted file mode 100644 index f3f75ddd9..000000000 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/EntityUtilTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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 org.apache.hugegraph.unit; - -import java.util.Date; - -import org.apache.hugegraph.entity.GraphConnection; -import org.apache.hugegraph.testutil.Assert; -import org.apache.hugegraph.util.EntityUtil; -import org.junit.Test; - -public class EntityUtilTest { - - @Test - public void testMerge() throws InterruptedException { - GraphConnection oldEntity; - GraphConnection newEntity; - oldEntity = new GraphConnection(1, "conn1", "graph1", "host1", 8001, - 30, "", "", true, "", - new Date(), "http", "", ""); - Thread.sleep(10); - newEntity = new GraphConnection(2, "conn2", "graph2", "host2", 8002, - 40, "u", "p", false, "xxx", - new Date(), "http", "", ""); - - GraphConnection entity = EntityUtil.merge(oldEntity, newEntity); - Assert.assertEquals(oldEntity.getId(), entity.getId()); - Assert.assertEquals(newEntity.getName(), entity.getName()); - Assert.assertEquals(newEntity.getGraph(), entity.getGraph()); - Assert.assertEquals(newEntity.getHost(), entity.getHost()); - Assert.assertEquals(newEntity.getPort(), entity.getPort()); - Assert.assertEquals(oldEntity.getTimeout(), entity.getTimeout()); - Assert.assertEquals(newEntity.getUsername(), entity.getUsername()); - Assert.assertEquals(newEntity.getPassword(), entity.getPassword()); - Assert.assertEquals(oldEntity.getCreateTime(), entity.getCreateTime()); - } -} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingSchemaTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingSchemaTest.java new file mode 100644 index 000000000..c90c9687e --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingSchemaTest.java @@ -0,0 +1,160 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; + +import org.junit.Test; +import org.springframework.core.io.FileSystemResource; +import org.springframework.jdbc.datasource.init.ScriptUtils; + +import org.apache.hugegraph.config.DatabaseSchemaMigrator; +import org.apache.hugegraph.testutil.Assert; + +public class FileMappingSchemaTest { + + @Test + public void testFileMappingStoresDeepUploadPath() throws Exception { + String url = "jdbc:h2:mem:file_mapping_deep_path;DB_CLOSE_DELAY=-1"; + try (Connection conn = DriverManager.getConnection(url)) { + ScriptUtils.executeSqlScript(conn, new FileSystemResource( + this.mainSchemaPath())); + + String deepPath = this.deepUploadPath(); + this.insertDeepPath(conn, deepPath); + + try (Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery( + "SELECT `path` FROM `file_mapping` " + + "WHERE `name` = 'deep.csv'")) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(deepPath, rs.getString(1)); + } + } + } + + @Test + public void testSchemaMigratorWidensExistingFileMappingPath() + throws Exception { + String url = "jdbc:h2:mem:file_mapping_legacy_path;DB_CLOSE_DELAY=-1"; + try (Connection conn = DriverManager.getConnection(url)) { + this.createLegacyFileMappingTable(conn); + + new DatabaseSchemaMigrator().migrate(conn); + + this.insertDeepPath(conn, this.deepUploadPath()); + } + } + + @Test + public void testSchemaMigratorAddsExecuteHistoryFailureReasonIdempotently() + throws Exception { + String url = "jdbc:h2:mem:execute_history_failure_reason;DB_CLOSE_DELAY=-1"; + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement statement = conn.createStatement()) { + statement.execute("CREATE TABLE `execute_history` (" + + "`id` INT NOT NULL AUTO_INCREMENT, " + + "PRIMARY KEY (`id`))"); + } + + DatabaseSchemaMigrator migrator = new DatabaseSchemaMigrator(); + migrator.migrate(conn); + migrator.migrate(conn); + + try (ResultSet columns = conn.getMetaData().getColumns( + null, null, "EXECUTE_HISTORY", "FAILURE_REASON")) { + Assert.assertTrue(columns.next()); + Assert.assertEquals(64, columns.getInt("COLUMN_SIZE")); + } + } + } + + private void insertDeepPath(Connection conn, String deepPath) + throws Exception { + try (PreparedStatement insert = conn.prepareStatement( + "INSERT INTO `file_mapping` " + + "(`graphspace`, `graph`, `job_id`, `name`, `path`, " + + "`total_lines`, `total_size`, `file_status`, " + + "`file_setting`, `vertex_mappings`, `edge_mappings`, " + + "`load_parameter`, `create_time`, `update_time`) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + + "CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)")) { + insert.setString(1, "DEFAULT"); + insert.setString(2, "hugegraph"); + insert.setInt(3, 1); + insert.setString(4, "deep.csv"); + insert.setString(5, deepPath); + insert.setLong(6, 1L); + insert.setLong(7, 1L); + insert.setInt(8, 0); + insert.setString(9, "{}"); + insert.setString(10, "[]"); + insert.setString(11, "[]"); + insert.setString(12, "{}"); + insert.executeUpdate(); + } + } + + private void createLegacyFileMappingTable(Connection conn) throws Exception { + try (Statement statement = conn.createStatement()) { + statement.execute("CREATE TABLE `file_mapping` (" + + "`id` INT NOT NULL AUTO_INCREMENT, " + + "`graphspace` VARCHAR(48) NOT NULL, " + + "`graph` VARCHAR(48) NOT NULL, " + + "`job_id` INT NOT NULL DEFAULT 0, " + + "`name` VARCHAR(128) NOT NULL, " + + "`path` VARCHAR(256) NOT NULL, " + + "`total_lines` LONG NOT NULL, " + + "`total_size` LONG NOT NULL, " + + "`file_status` TINYINT NOT NULL DEFAULT 0, " + + "`file_setting` VARCHAR(65535) NOT NULL, " + + "`vertex_mappings` VARCHAR(65535) NOT NULL, " + + "`edge_mappings` VARCHAR(65535) NOT NULL, " + + "`load_parameter` VARCHAR(65535) NOT NULL, " + + "`create_time` DATETIME(6) NOT NULL, " + + "`update_time` DATETIME(6) NOT NULL, " + + "PRIMARY KEY (`id`))"); + } + } + + private Path mainSchemaPath() { + Path modulePath = Paths.get("src/main/resources/database/schema.sql"); + if (Files.exists(modulePath)) { + return modulePath; + } + return Paths.get("hugegraph-hubble/hubble-be/src/main/resources/" + + "database/schema.sql"); + } + + private String deepUploadPath() { + StringBuilder builder = new StringBuilder("/tmp/hubble-upload"); + while (builder.length() < 600) { + builder.append("/nested-directory"); + } + builder.append("/deep.csv"); + return builder.toString(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileUploadControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileUploadControllerTest.java new file mode 100644 index 000000000..d6b251789 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileUploadControllerTest.java @@ -0,0 +1,346 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.controller.load.FileUploadController; +import org.apache.hugegraph.entity.enums.FileMappingStatus; +import org.apache.hugegraph.entity.enums.JobStatus; +import org.apache.hugegraph.entity.load.FileMapping; +import org.apache.hugegraph.entity.load.JobManager; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.load.FileMappingService; +import org.apache.hugegraph.service.load.JobManagerService; +import org.apache.hugegraph.testutil.Assert; + +public class FileUploadControllerTest { + + @Test + public void testCheckFileValidAcceptsUppercaseTrimmedFormat() + throws Exception { + FileUploadController controller = this.controller(" CSV ", " TXT "); + MockMultipartFile file = new MockMultipartFile("file", "HLM.TXT", + "text/plain", + "name\nmarko".getBytes()); + JobManager job = JobManager.builder() + .id(1) + .jobStatus(JobStatus.UPLOADING) + .build(); + + this.checkFileValid(controller, job, file, "HLM.TXT"); + } + + @Test + public void testCheckFileValidSkipsNullAndBlankWhitelistItems() + throws Exception { + FileUploadController controller = this.controller(" CSV ", null, " ", + " TXT "); + MockMultipartFile file = new MockMultipartFile("file", "HLM.TXT", + "text/plain", + "name\nmarko".getBytes()); + JobManager job = JobManager.builder() + .id(1) + .jobStatus(JobStatus.UPLOADING) + .build(); + + this.checkFileValid(controller, job, file, "HLM.TXT"); + } + + @Test + public void testCheckFileValidRejectsMissingExtension() throws Exception { + FileUploadController controller = this.controller("csv", "txt"); + MockMultipartFile file = new MockMultipartFile("file", "HLM", + "text/plain", + "name\nmarko".getBytes()); + JobManager job = JobManager.builder() + .id(1) + .jobStatus(JobStatus.UPLOADING) + .build(); + + Assert.assertThrows(ExternalException.class, () -> { + this.checkFileValid(controller, job, file, "HLM"); + }); + } + + @Test + public void testCheckFileValidRejectsEmptyWhitelist() throws Exception { + FileUploadController controller = this.controller((String[]) null); + MockMultipartFile file = new MockMultipartFile("file", "HLM.TXT", + "text/plain", + "name\nmarko".getBytes()); + JobManager job = JobManager.builder() + .id(1) + .jobStatus(JobStatus.UPLOADING) + .build(); + + Assert.assertThrows(ExternalException.class, () -> { + this.checkFileValid(controller, job, file, "HLM.TXT"); + }); + } + + @Test + public void testReserveUploadQuotaUsesGraphRouteForNewMapping() + throws Exception { + FileUploadController controller = this.controller("csv"); + FileMappingService service = Mockito.mock(FileMappingService.class); + JobManagerService jobService = Mockito.mock(JobManagerService.class); + JobManager job = JobManager.builder().id(1).jobSize(20L).build(); + Mockito.when(jobService.get("DEFAULT", "hugegraph", 1)) + .thenReturn(job); + Mockito.when(service.get("DEFAULT", "hugegraph", 1, "data.csv")) + .thenReturn(null); + Mockito.when(service.listByJob(1)).thenReturn(Collections.emptyList()); + this.setField(controller, "service", service); + this.setField(controller, "jobService", jobService); + + FileMapping mapping = this.reserveUploadQuota(controller, "DEFAULT", + "hugegraph", 1, + "data.csv", + "upload/data.csv", 10L); + + ArgumentCaptor captor = ArgumentCaptor.forClass( + FileMapping.class); + Mockito.verify(service).save(captor.capture()); + Assert.assertEquals(mapping, captor.getValue()); + Assert.assertEquals("DEFAULT", mapping.getGraphSpace()); + Assert.assertEquals("hugegraph", mapping.getGraph()); + Assert.assertEquals("data.csv", mapping.getName()); + Assert.assertEquals("upload/data.csv", mapping.getPath()); + Assert.assertEquals(10L, mapping.getTotalSize()); + Assert.assertEquals(FileMappingStatus.UPLOADING, + mapping.getFileStatus()); + } + + @Test + public void testReserveUploadQuotaRejectsCompletedDuplicate() + throws Exception { + FileUploadController controller = this.controller("csv"); + FileMappingService service = Mockito.mock(FileMappingService.class); + JobManagerService jobService = Mockito.mock(JobManagerService.class); + JobManager job = JobManager.builder().id(1).jobSize(20L).build(); + FileMapping completed = new FileMapping("DEFAULT", "hugegraph", + "data.csv", + "upload/old.csv"); + completed.setFileStatus(FileMappingStatus.COMPLETED); + Mockito.when(jobService.get("DEFAULT", "hugegraph", 1)) + .thenReturn(job); + Mockito.when(service.get("DEFAULT", "hugegraph", 1, "data.csv")) + .thenReturn(completed); + this.setField(controller, "service", service); + this.setField(controller, "jobService", jobService); + + Assert.assertThrows(ExternalException.class, () -> { + this.reserveUploadQuota(controller, "DEFAULT", "hugegraph", 1, + "data.csv", "upload/data.csv", 10L); + }); + Mockito.verify(service, Mockito.never()).save(Mockito.any()); + Mockito.verify(service, Mockito.never()).update(Mockito.any()); + } + + @Test + public void testReserveUploadQuotaUpdatesExistingUploadingMapping() + throws Exception { + FileUploadController controller = this.controller("csv"); + FileMappingService service = Mockito.mock(FileMappingService.class); + JobManagerService jobService = Mockito.mock(JobManagerService.class); + JobManager job = JobManager.builder().id(1).jobSize(20L).build(); + FileMapping existing = new FileMapping("DEFAULT", "hugegraph", + "data.csv", + "upload/old.csv"); + existing.setId(2); + existing.setJobId(1); + existing.setTotalSize(5L); + existing.setFileStatus(FileMappingStatus.UPLOADING); + List uploading = new ArrayList<>(); + uploading.add(existing); + Mockito.when(jobService.get("DEFAULT", "hugegraph", 1)) + .thenReturn(job); + Mockito.when(service.get("DEFAULT", "hugegraph", 1, "data.csv")) + .thenReturn(existing); + Mockito.when(service.listByJob(1)).thenReturn(uploading); + this.setField(controller, "service", service); + this.setField(controller, "jobService", jobService); + + FileMapping mapping = this.reserveUploadQuota(controller, "DEFAULT", + "hugegraph", 1, + "data.csv", + "upload/data.csv", 10L); + + Assert.assertEquals(existing, mapping); + Assert.assertEquals("upload/data.csv", mapping.getPath()); + Assert.assertEquals(10L, mapping.getTotalSize()); + Assert.assertEquals(FileMappingStatus.UPLOADING, + mapping.getFileStatus()); + Mockito.verify(service).update(existing); + Mockito.verify(service, Mockito.never()).save(Mockito.any()); + } + + @Test + public void testCheckTotalAndIndexValidRejectsIndexAtTotal() + throws Exception { + FileUploadController controller = this.controller("csv"); + + Assert.assertThrows(InternalException.class, () -> { + this.checkTotalAndIndexValid(controller, 2, 2); + }); + } + + @Test + public void testUploadFileUsesValidatedFileNameForPartPath() + throws Exception { + Path tempDir = Files.createTempDirectory("hubble-upload"); + try { + FileMappingService service = new FileMappingService(); + MockMultipartFile file = new MockMultipartFile( + "file", "../evil.csv", "text/csv", "id,name".getBytes()); + + service.uploadFile(file, "safe.csv", 0, tempDir.toString()); + + Assert.assertTrue(Files.exists(tempDir.resolve("safe.csv-0"))); + Assert.assertFalse(Files.exists(tempDir.resolve("evil.csv-0"))); + Assert.assertFalse(Files.exists(tempDir.getParent() + .resolve("evil.csv-0"))); + } finally { + FileUtils.deleteDirectory(tempDir.toFile()); + } + } + + @Test + public void testTryMergePartFilesReplacesStaleAllFile() throws Exception { + Path parent = Files.createTempDirectory("hubble-merge"); + File dest = parent.resolve("data.csv").toFile(); + File all = parent.resolve("data.csv.all").toFile(); + File partDir = dest; + try { + FileUtils.forceMkdir(partDir); + Files.write(all.toPath(), "stale".getBytes()); + Files.write(new File(partDir, "data.csv-0").toPath(), + "new".getBytes()); + + FileMappingService service = new FileMappingService(); + Assert.assertTrue(service.tryMergePartFiles(partDir.toString(), 1)); + + Assert.assertEquals("new", + new String(Files.readAllBytes(dest.toPath()))); + } finally { + FileUtils.deleteDirectory(parent.toFile()); + } + } + + private FileUploadController controller(String... formats) throws Exception { + FileUploadController controller = new FileUploadController(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.UPLOAD_FILE_FORMAT_LIST)) + .thenReturn(formats == null ? null : Arrays.asList(formats)); + Mockito.when(config.get(HubbleOptions.UPLOAD_SINGLE_FILE_SIZE_LIMIT)) + .thenReturn(1024L); + Mockito.when(config.get(HubbleOptions.UPLOAD_TOTAL_FILE_SIZE_LIMIT)) + .thenReturn(2048L); + this.setField(controller, "config", config); + return controller; + } + + private void checkFileValid(FileUploadController controller, JobManager job, + MockMultipartFile file, String fileName) + throws Exception { + Method method = FileUploadController.class.getDeclaredMethod("checkFileValid", + String.class, + String.class, + int.class, + JobManager.class, + org.springframework.web.multipart.MultipartFile.class, + String.class); + method.setAccessible(true); + try { + method.invoke(controller, "DEFAULT", "hugegraph", 1, job, file, + fileName); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw e; + } + } + + private FileMapping reserveUploadQuota(FileUploadController controller, + String graphSpace, String graph, + int jobId, String fileName, + String filePath, + Long sourceFileSize) + throws Exception { + Method method = FileUploadController.class.getDeclaredMethod( + "reserveUploadQuota", String.class, String.class, + int.class, String.class, String.class, Long.class); + method.setAccessible(true); + try { + return (FileMapping) method.invoke(controller, graphSpace, graph, + jobId, fileName, filePath, + sourceFileSize); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw e; + } + } + + private void checkTotalAndIndexValid(FileUploadController controller, + int total, int index) + throws Exception { + Method method = FileUploadController.class.getDeclaredMethod( + "checkTotalAndIndexValid", int.class, int.class); + method.setAccessible(true); + try { + method.invoke(controller, total, index); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw e; + } + } + + private void setField(Object object, String name, Object value) throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileUtilTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileUtilTest.java index ac959bf14..59c10010f 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileUtilTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileUtilTest.java @@ -18,10 +18,11 @@ package org.apache.hugegraph.unit; -import org.apache.hugegraph.testutil.Assert; -import org.apache.hugegraph.util.FileUtil; import org.junit.Test; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.FileUtil;; + public class FileUtilTest { @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphConnectionTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphConnectionTest.java deleted file mode 100644 index aa2196093..000000000 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphConnectionTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 org.apache.hugegraph.unit; - -import org.apache.hugegraph.HugeGraphHubble; -import org.apache.hugegraph.common.Constant; -import org.apache.hugegraph.common.Response; -import org.apache.hugegraph.entity.GraphConnection; -import org.apache.hugegraph.testutil.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@TestPropertySource("classpath:application.properties") -@SpringBootTest(classes = HugeGraphHubble.class, webEnvironment = - SpringBootTest.WebEnvironment.RANDOM_PORT) -public class GraphConnectionTest { - - private static final String HOST = "127.0.0.1"; - private static final int PORT = 8080; - - @Autowired - private TestRestTemplate testRestTemplate; - - @Test - public void testGraphConnect() { - GraphConnection entry = - GraphConnection.builder().host(HOST).port(PORT).name("test").graph( - "hugegraph").build(); - Response response = testRestTemplate.postForObject( - Constant.API_VERSION + "graph-connections", - entry, Response.class); - Assert.assertEquals(response.getMessage(), 200, response.getStatus()); - } -} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphMetricsControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphMetricsControllerTest.java new file mode 100644 index 000000000..34be2371a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphMetricsControllerTest.java @@ -0,0 +1,51 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.apache.hugegraph.controller.saas.GraphMetricsController; +import org.apache.hugegraph.controller.saas.GraphMetricsController.SchemaCount; +import org.apache.hugegraph.testutil.Assert; + +public class GraphMetricsControllerTest { + + @Test + public void testWeeklyGrowthRateAcceptsSparseTypeCounts() { + List dateRange = Arrays.asList( + "20260701", "20260702", "20260703", "20260704", + "20260705", "20260706", "20260707", "20260708", + "20260709", "20260710", "20260711", "20260712", + "20260713", "20260714" + ); + SchemaCount today = new SchemaCount<>(1, 2, 3, 4); + + SchemaCount result = GraphMetricsController.weeklyGrowthRate( + Collections.emptyMap(), dateRange, today); + + Assert.assertEquals(1.0, result.pk); + Assert.assertEquals(1.0, result.vl); + Assert.assertEquals(1.0, result.el); + Assert.assertEquals(1.0, result.il); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphServiceImportTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphServiceImportTest.java new file mode 100644 index 000000000..4b03feb0a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphServiceImportTest.java @@ -0,0 +1,508 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.io.File; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import org.apache.hugegraph.driver.GraphManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.schema.EdgeLabelEntity; +import org.apache.hugegraph.entity.schema.VertexLabelEntity; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.service.graph.GraphService; +import org.apache.hugegraph.service.schema.EdgeLabelService; +import org.apache.hugegraph.service.schema.PropertyKeyService; +import org.apache.hugegraph.service.schema.VertexLabelService; +import org.apache.hugegraph.structure.constant.IdStrategy; +import org.apache.hugegraph.structure.graph.Edge; +import org.apache.hugegraph.structure.graph.Vertex; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.Bytes; + +public class GraphServiceImportTest { + + @Test + public void testImportJsonRejectsOversizedFileBeforeMaterializing() + throws Exception { + GraphService service = this.serviceWithSchemas(); + HugeClient client = this.clientWithGraph(Mockito.mock(GraphManager.class)); + MultipartFile file = Mockito.mock(MultipartFile.class); + Mockito.when(file.isEmpty()).thenReturn(false); + Mockito.when(file.getSize()).thenReturn(Bytes.GB + 1L); + + Assert.assertThrows(ExternalException.class, () -> { + service.importJson(client, file); + }); + + UserService userService = (UserService) this.getField(service, + "userService"); + Mockito.verify(userService, Mockito.never()) + .multipartFileToFile(Mockito.any()); + } + + @Test + public void testImportJsonValidatesEdgesBeforeWritingVertices() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + Vertex source = new Vertex("person"); + source.id("source"); + Mockito.when(graph.getVertex("source")).thenReturn(source); + Mockito.when(graph.getVertex("missing")).thenReturn(null); + + String json = "{" + + "\"vertices\":[" + + "{\"id\":\"source\",\"label\":\"person\"," + + "\"properties\":{}}]," + + "\"edges\":[" + + "{\"label\":\"knows\",\"source\":\"source\"," + + "\"target\":\"missing\",\"properties\":{}}]" + + "}"; + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file(json)); + }); + + Assert.assertEquals("gremlin.edges.linked-vertex.not-exist", + e.getMessage()); + Mockito.verify(graph, Mockito.never()) + .addVertex(Mockito.any(Vertex.class)); + Mockito.verify(graph, Mockito.never()).addEdge(Mockito.any()); + } + + @Test + public void testImportJsonRejectsLabelMismatchBeforeWriting() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + Vertex source = new Vertex("person"); + source.id("source"); + Vertex target = new Vertex("software"); + target.id("target"); + Mockito.when(graph.getVertex("target")).thenReturn(target); + + String json = "{" + + "\"vertices\":[" + + "{\"id\":\"source\",\"label\":\"person\"," + + "\"properties\":{}}]," + + "\"edges\":[" + + "{\"label\":\"knows\",\"source\":\"source\"," + + "\"target\":\"target\",\"properties\":{}}]" + + "}"; + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file(json)); + }); + + Assert.assertEquals("graph.edge.link-unmatched-vertex", + e.getMessage()); + Mockito.verify(graph, Mockito.never()) + .addVertex(Mockito.any(Vertex.class)); + Mockito.verify(graph, Mockito.never()).addEdge(Mockito.any()); + } + + @Test + public void testImportJsonReportsMissingSourceAndTargetAsRequired() + throws Exception { + GraphService service = this.serviceWithSchemas(); + HugeClient client = + this.clientWithGraph(Mockito.mock(GraphManager.class)); + String json = "{" + + "\"vertices\":[]," + + "\"edges\":[" + + "{\"label\":\"knows\",\"target\":\"target\"," + + "\"properties\":{}}]" + + "}"; + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file(json)); + }); + + Assert.assertEquals("common.param.cannot-be-null", e.getMessage()); + Assert.assertEquals("source_id", e.args()[0]); + } + + @Test + public void testImportJsonReportsMissingTargetAsRequired() + throws Exception { + GraphService service = this.serviceWithSchemas(); + HugeClient client = + this.clientWithGraph(Mockito.mock(GraphManager.class)); + String json = "{" + + "\"vertices\":[]," + + "\"edges\":[" + + "{\"label\":\"knows\",\"source\":\"source\"," + + "\"properties\":{}}]" + + "}"; + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file(json)); + }); + + Assert.assertEquals("common.param.cannot-be-null", e.getMessage()); + Assert.assertEquals("target_id", e.args()[0]); + } + + @Test + public void testImportJsonRejectsInvalidJsonAsExternalError() + throws Exception { + GraphService service = this.serviceWithSchemas(); + HugeClient client = + this.clientWithGraph(Mockito.mock(GraphManager.class)); + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file("{invalid json")); + }); + + Assert.assertEquals("graph.import.invalid-json", e.getMessage()); + } + + @Test + public void testImportJsonRejectsMissingEdgesField() + throws Exception { + GraphService service = this.serviceWithSchemas(); + HugeClient client = + this.clientWithGraph(Mockito.mock(GraphManager.class)); + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file("{\"vertices\":[]}")); + }); + + Assert.assertEquals("graph.import.missing-field", e.getMessage()); + Assert.assertEquals("edges", e.args()[0]); + } + + @Test + public void testImportJsonRejectsMissingVerticesField() + throws Exception { + GraphService service = this.serviceWithSchemas(); + HugeClient client = + this.clientWithGraph(Mockito.mock(GraphManager.class)); + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file("{\"edges\":[]}")); + }); + + Assert.assertEquals("graph.import.missing-field", e.getMessage()); + Assert.assertEquals("vertices", e.args()[0]); + } + + @Test + public void testImportJsonRejectsNonArrayFields() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + + ExternalException verticesError = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file("{\"vertices\":{},\"edges\":[]}")); + }); + + Assert.assertEquals("graph.import.field-should-array", + verticesError.getMessage()); + Assert.assertEquals("vertices", verticesError.args()[0]); + + ExternalException edgesError = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file("{\"vertices\":[],\"edges\":{}}")); + }); + + Assert.assertEquals("graph.import.field-should-array", + edgesError.getMessage()); + Assert.assertEquals("edges", edgesError.args()[0]); + Mockito.verify(graph, Mockito.never()) + .addVertex(Mockito.any(Vertex.class)); + Mockito.verify(graph, Mockito.never()).addEdge(Mockito.any()); + } + + @Test + public void testImportJsonRejectsDuplicateVertexId() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + String json = "{" + + "\"vertices\":[" + + "{\"id\":\"source\",\"label\":\"person\"," + + "\"properties\":{}}," + + "{\"id\":\"source\",\"label\":\"person\"," + + "\"properties\":{}}]," + + "\"edges\":[]" + + "}"; + + ExternalException e = (ExternalException) + Assert.assertThrows(ExternalException.class, + () -> { + service.importJson(client, this.file(json)); + }); + + Assert.assertEquals("graph.import.vertex.duplicate-id", + e.getMessage()); + Assert.assertEquals("source", e.args()[0]); + Mockito.verify(graph, Mockito.never()) + .addVertex(Mockito.any(Vertex.class)); + Mockito.verify(graph, Mockito.never()).addEdge(Mockito.any()); + } + + @Test + public void testImportJsonAddsValidatedVerticesAndEdges() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + Mockito.when(graph.addVertex(Mockito.any(Vertex.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(graph.addEdge(Mockito.any(Edge.class))) + .thenAnswer(invocation -> { + Edge edge = invocation.getArgument(0); + edge.id("S1:source>target"); + return edge; + }); + String json = "{" + + "\"vertices\":[" + + "{\"id\":\"source\",\"label\":\"person\"," + + "\"properties\":{}}," + + "{\"id\":\"target\",\"label\":\"person\"," + + "\"properties\":{}}]," + + "\"edges\":[" + + "{\"id\":\"edge-1\",\"label\":\"knows\"," + + "\"source\":\"source\"," + + "\"target\":\"target\",\"properties\":{}}]" + + "}"; + + org.apache.hugegraph.entity.query.GraphView graphView = + service.importJson(client, this.file(json)); + + Assert.assertEquals(2, graphView.getVertices().size()); + Assert.assertEquals(1, graphView.getEdges().size()); + Mockito.verify(graph, Mockito.never()).getVertex("source"); + Mockito.verify(graph, Mockito.never()).getVertex("target"); + } + + @Test + public void testImportJsonLinksPrimaryKeyVerticesFromSameFile() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + final int[] vertexIndex = {0}; + Mockito.when(graph.addVertex(Mockito.any(Vertex.class))) + .thenAnswer(invocation -> { + Vertex input = invocation.getArgument(0); + Vertex created = new Vertex(input.label()); + created.id(vertexIndex[0]++ == 0 ? "account-source" : + "account-target"); + return created; + }); + Mockito.when(graph.addEdge(Mockito.any(Edge.class))) + .thenAnswer(invocation -> { + Edge edge = invocation.getArgument(0); + edge.id("Saccount-source>target"); + return edge; + }); + String json = "{" + + "\"vertices\":[" + + "{\"id\":\"account-source\",\"label\":\"account\"," + + "\"properties\":{}}," + + "{\"id\":\"account-target\",\"label\":\"account\"," + + "\"properties\":{}}]," + + "\"edges\":[" + + "{\"id\":\"edge-1\",\"label\":\"follows\"," + + "\"source\":\"account-source\"," + + "\"target\":\"account-target\",\"properties\":{}}]" + + "}"; + + org.apache.hugegraph.entity.query.GraphView graphView = + service.importJson(client, this.file(json)); + + Assert.assertEquals(2, graphView.getVertices().size()); + Assert.assertEquals(1, graphView.getEdges().size()); + Mockito.verify(graph, Mockito.never()).getVertex("account-source"); + Mockito.verify(graph, Mockito.never()).getVertex("account-target"); + } + + @Test + public void testImportJsonRollsBackCreatedVerticesWhenEdgeWriteFails() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + Mockito.when(graph.addVertex(Mockito.any(Vertex.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(graph.addEdge(Mockito.any(Edge.class))) + .thenThrow(new RuntimeException("write edge failed")); + String json = "{" + + "\"vertices\":[" + + "{\"id\":\"source\",\"label\":\"person\"," + + "\"properties\":{}}," + + "{\"id\":\"target\",\"label\":\"person\"," + + "\"properties\":{}}]," + + "\"edges\":[" + + "{\"label\":\"knows\",\"source\":\"source\"," + + "\"target\":\"target\",\"properties\":{}}]" + + "}"; + + Assert.assertThrows(RuntimeException.class, () -> { + service.importJson(client, this.file(json)); + }); + + Mockito.verify(graph).deleteVertex("source"); + Mockito.verify(graph).deleteVertex("target"); + } + + @Test + public void testImportJsonRollsBackCreatedEdgesBeforeVertices() + throws Exception { + GraphService service = this.serviceWithSchemas(); + GraphManager graph = Mockito.mock(GraphManager.class); + HugeClient client = this.clientWithGraph(graph); + Mockito.when(graph.addVertex(Mockito.any(Vertex.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(graph.addEdge(Mockito.any(Edge.class))) + .thenAnswer(invocation -> { + Edge edge = invocation.getArgument(0); + edge.id("S1:source>target"); + return edge; + }) + .thenThrow(new RuntimeException("write edge failed")); + String json = "{" + + "\"vertices\":[" + + "{\"id\":\"source\",\"label\":\"person\"," + + "\"properties\":{}}," + + "{\"id\":\"target\",\"label\":\"person\"," + + "\"properties\":{}}]," + + "\"edges\":[" + + "{\"label\":\"knows\",\"source\":\"source\"," + + "\"target\":\"target\",\"properties\":{}}," + + "{\"label\":\"knows\",\"source\":\"target\"," + + "\"target\":\"source\",\"properties\":{}}]" + + "}"; + + Assert.assertThrows(RuntimeException.class, () -> { + service.importJson(client, this.file(json)); + }); + + Mockito.verify(graph).deleteEdge("S1:source>target"); + Mockito.verify(graph).deleteVertex("source"); + Mockito.verify(graph).deleteVertex("target"); + } + + private GraphService serviceWithSchemas() throws Exception { + GraphService service = new GraphService(); + + UserService userService = Mockito.mock(UserService.class); + Mockito.when(userService.multipartFileToFile(Mockito.any())) + .thenAnswer(invocation -> { + MultipartFile file = invocation.getArgument(0); + File temp = File.createTempFile("hubble-import", ".json"); + file.transferTo(temp); + return temp; + }); + this.setField(service, "userService", userService); + + VertexLabelService vlService = Mockito.mock(VertexLabelService.class); + Mockito.when(vlService.get(Mockito.eq("person"), Mockito.any())) + .thenReturn(VertexLabelEntity.builder() + .name("person") + .idStrategy(IdStrategy.CUSTOMIZE_STRING) + .properties(Collections.emptySet()) + .build()); + Mockito.when(vlService.get(Mockito.eq("account"), Mockito.any())) + .thenReturn(VertexLabelEntity.builder() + .name("account") + .idStrategy(IdStrategy.PRIMARY_KEY) + .properties(Collections.emptySet()) + .build()); + this.setField(service, "vlService", vlService); + + EdgeLabelService elService = Mockito.mock(EdgeLabelService.class); + Mockito.when(elService.get(Mockito.eq("knows"), Mockito.any())) + .thenReturn(EdgeLabelEntity.builder() + .name("knows") + .sourceLabel("person") + .targetLabel("person") + .properties(Collections.emptySet()) + .build()); + Mockito.when(elService.get(Mockito.eq("follows"), Mockito.any())) + .thenReturn(EdgeLabelEntity.builder() + .name("follows") + .sourceLabel("account") + .targetLabel("account") + .properties(Collections.emptySet()) + .build()); + this.setField(service, "elService", elService); + + this.setField(service, "pkService", + Mockito.mock(PropertyKeyService.class)); + return service; + } + + private HugeClient clientWithGraph(GraphManager graph) { + HugeClient client = Mockito.mock(HugeClient.class); + Mockito.when(client.graph()).thenReturn(graph); + return client; + } + + private MultipartFile file(String content) { + return new MockMultipartFile("file", "graph.json", + "application/json", + content.getBytes(StandardCharsets.UTF_8)); + } + + private void setField(Object object, String name, Object value) + throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } + + private Object getField(Object object, String name) throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(object); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java new file mode 100644 index 000000000..d22567261 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java @@ -0,0 +1,229 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.util.NestedServletException; + +import org.apache.hugegraph.controller.graphs.GraphsController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.service.graphs.GraphsService; +import org.apache.hugegraph.testutil.Assert; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class GraphsControllerCanonicalTest { + + private MockMvc mvc; + private HugeClient client; + private GraphsService graphsService; + private RequestPostProcessor withClient; + + @Before + public void setup() throws Exception { + GraphsController controller = new GraphsController(); + this.graphsService = Mockito.mock(GraphsService.class); + this.setField(controller, "graphsService", this.graphsService); + + this.client = Mockito.mock(HugeClient.class); + this.withClient = request -> { + request.setAttribute("hugeClient", this.client); + return request; + }; + this.mvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + public void testCanonicalCreateGraphUsesPathNameAndJsonBody() + throws Exception { + Mockito.when(this.graphsService.create(Mockito.eq(this.client), + Mockito.eq("GraphNick"), + Mockito.eq("graph_a"), + Mockito.eq("template_a"))) + .thenReturn(Collections.singletonMap("name", "graph_a")); + + this.mvc.perform(post("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a") + .with(this.withClient) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"nickname\":\"GraphNick\"," + + "\"schema\":\"template_a\"}")) + .andExpect(status().isOk()); + + Mockito.verify(this.graphsService) + .create(this.client, "GraphNick", "graph_a", "template_a"); + } + + @Test + public void testCanonicalUpdateGraphUsesJsonBody() + throws Exception { + this.mvc.perform(put("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a") + .with(this.withClient) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"nickname\":\"GraphNick\"}")) + .andExpect(status().isOk()); + + Mockito.verify(this.graphsService) + .update(this.client, "GraphNick", "graph_a"); + } + + @Test + public void testCanonicalClearGraphUsesPostAndClearsSchemaAndData() + throws Exception { + this.mvc.perform(post("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/clear") + .with(this.withClient)) + .andExpect(status().isOk()); + + Mockito.verify(this.graphsService).clearGraph(this.client, "graph_a"); + } + + @Test + public void testCanonicalClearGraphPropagatesDefaultGraphRejection() + throws Exception { + Mockito.doThrow(new ExternalException("Can't clear default graph")) + .when(this.graphsService).clearGraph(this.client, "graph_a"); + + try { + this.mvc.perform( + post("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/clear") + .with(this.withClient)); + org.junit.Assert.fail("Expected default graph rejection"); + } catch (NestedServletException e) { + Assert.assertInstanceOf(ExternalException.class, e.getCause()); + } + + Mockito.verify(this.graphsService).clearGraph(this.client, "graph_a"); + } + + @Test + public void testLegacyGetClearGraphRouteIsNotSuccessful() + throws Exception { + this.mvc.perform(get("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/truncate") + .with(this.withClient) + .param("clear_schema", "true")) + .andExpect(status().is4xxClientError()); + + Mockito.verifyZeroInteractions(this.graphsService); + } + + @Test + public void testLegacyGraphMutationRoutesAreNotSuccessful() + throws Exception { + this.mvc.perform(get("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/update") + .with(this.withClient) + .param("nickname", "GraphNick")) + .andExpect(status().is4xxClientError()); + this.mvc.perform(get("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/setdefault") + .with(this.withClient)) + .andExpect(status().is4xxClientError()); + this.mvc.perform(get("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/unsetdefault") + .with(this.withClient)) + .andExpect(status().is4xxClientError()); + this.mvc.perform(get("/api/v1.3/graphspaces/DEFAULT/graphs/getdefault") + .with(this.withClient)) + .andExpect(status().is4xxClientError()); + this.mvc.perform(post("/api/v1.3/graphspaces/DEFAULT/graphs") + .with(this.withClient) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("graph", "graph_b") + .param("nickname", "LegacyNick") + .param("schema", "template_b")) + .andExpect(status().is4xxClientError()); + } + + @Test + public void testCanonicalSetDefaultGraphUsesServiceDefault() + throws Exception { + this.mvc.perform(post("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/default") + .with(this.withClient)) + .andExpect(status().isOk()); + + Mockito.verify(this.graphsService).setDefault(this.client, "graph_a"); + } + + @Test + public void testSetDefaultUsesGraphspaceScopedClient() throws Exception { + ScopeCapturingController controller = new ScopeCapturingController(); + controller.client = this.client; + this.setField(controller, "graphsService", this.graphsService); + + controller.setDefaultByCanonicalApi("DEFAULT", "graph_a"); + + Assert.assertEquals("DEFAULT", controller.graphspace); + Assert.assertNull(controller.graph); + Mockito.verify(this.graphsService).setDefault(this.client, "graph_a"); + } + + @Test + public void testCanonicalUnsetDefaultGraphUsesServiceDefault() + throws Exception { + this.mvc.perform(delete("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/default") + .with(this.withClient)) + .andExpect(status().isOk()); + + Mockito.verify(this.graphsService).unSetDefault(this.client, "graph_a"); + } + + @Test + public void testCanonicalGetDefaultGraphUsesServiceDefault() + throws Exception { + Mockito.when(this.graphsService.getDefault(this.client)) + .thenReturn(Collections.singletonMap("name", "graph_a")); + + this.mvc.perform(get("/api/v1.3/graphspaces/DEFAULT/graphs/default") + .with(this.withClient)) + .andExpect(status().isOk()); + + Mockito.verify(this.graphsService).getDefault(this.client); + } + + private void setField(Object object, String name, Object value) + throws Exception { + Field field = GraphsController.class.getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } + + private static class ScopeCapturingController extends GraphsController { + + private HugeClient client; + private String graphspace; + private String graph; + + @Override + protected HugeClient authClient(String graphspace, String graph) { + this.graphspace = graphspace; + this.graph = graph; + return this.client; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java new file mode 100644 index 000000000..341cc80b3 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import org.apache.hugegraph.driver.GraphsManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.service.graphs.GraphsService; + +public class GraphsServiceDefaultTest { + + private HugeClient client; + private GraphsManager graphs; + private GraphsService service; + + @Before + public void setup() { + this.client = Mockito.mock(HugeClient.class); + this.graphs = Mockito.mock(GraphsManager.class); + Mockito.when(this.client.graphs()).thenReturn(this.graphs); + this.service = new GraphsService(); + } + + @Test + public void testSetDefaultReplacesAllPreviousDefaults() { + Map defaults = new LinkedHashMap<>(); + defaults.put("default_graph", Arrays.asList("old_a", "old_b")); + Mockito.when(this.graphs.getDefault()).thenReturn(defaults); + + this.service.setDefault(this.client, "target"); + + InOrder order = Mockito.inOrder(this.graphs); + order.verify(this.graphs).getDefault(); + order.verify(this.graphs).setDefault("target"); + order.verify(this.graphs).unSetDefault("old_a"); + order.verify(this.graphs).unSetDefault("old_b"); + } + + @Test + public void testClearGraphUsesExplicitDestructiveConfirmation() { + Mockito.when(this.graphs.getDefault()).thenReturn( + Collections.singletonMap("default_graph", + Collections.singletonList("default"))); + + this.service.clearGraph(this.client, "graph_a"); + + InOrder order = Mockito.inOrder(this.graphs); + order.verify(this.graphs).getDefault(); + order.verify(this.graphs).clearGraph( + "graph_a", "I'm sure to delete all data"); + } + + @Test + public void testClearGraphRejectsDefaultGraph() { + Mockito.when(this.graphs.getDefault()).thenReturn( + Collections.singletonMap("default_graph", + Arrays.asList("other", "graph_a"))); + + try { + this.service.clearGraph(this.client, "graph_a"); + Assert.fail("Expected default graph rejection"); + } catch (ExternalException ignored) { + // Expected: default graphs must not be cleared. + } + + Mockito.verify(this.graphs).getDefault(); + Mockito.verify(this.graphs, Mockito.never()) + .clearGraph(Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testSetDefaultIsNoopWhenTargetIsOnlyDefault() { + Mockito.when(this.graphs.getDefault()).thenReturn( + Collections.singletonMap("default_graph", + Collections.singletonList("target"))); + + this.service.setDefault(this.client, "target"); + + Mockito.verify(this.graphs).getDefault(); + Mockito.verify(this.graphs, Mockito.never()).unSetDefault(Mockito.anyString()); + Mockito.verify(this.graphs, Mockito.never()).setDefault(Mockito.anyString()); + } + + @Test + public void testSetDefaultKeepsTargetAndDeduplicatesOldDefaults() { + Map defaults = new LinkedHashMap<>(); + defaults.put("default_graph", Arrays.asList("old", "target", "old")); + Mockito.when(this.graphs.getDefault()).thenReturn(defaults); + + this.service.setDefault(this.client, "target"); + + Mockito.verify(this.graphs, Mockito.never()).setDefault(Mockito.anyString()); + Mockito.verify(this.graphs).unSetDefault("old"); + Mockito.verify(this.graphs, Mockito.never()).unSetDefault("target"); + } + + @Test + public void testSetFailureDoesNotClearExistingDefault() { + Map defaults = new LinkedHashMap<>(); + defaults.put("default_graph", Collections.singletonList("old")); + Mockito.when(this.graphs.getDefault()).thenReturn(defaults); + Mockito.when(this.graphs.setDefault("target")) + .thenThrow(new IllegalStateException("set failed")); + + try { + this.service.setDefault(this.client, "target"); + Assert.fail("Expected set failure"); + } catch (IllegalStateException ignored) { + // Expected: the failure propagates without clearing the old default. + } + + Mockito.verify(this.graphs, Mockito.never()).unSetDefault(Mockito.anyString()); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GremlinHistoryFailureTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GremlinHistoryFailureTest.java new file mode 100644 index 000000000..00ad8fefe --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GremlinHistoryFailureTest.java @@ -0,0 +1,77 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import org.apache.hugegraph.controller.query.GremlinQueryController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.ExecuteHistory; +import org.apache.hugegraph.entity.query.GremlinQuery; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class GremlinHistoryFailureTest { + + @Test + public void testSyncFailurePersistsOnlyControlledReasonCode() { + HugeClient client = Mockito.mock(HugeClient.class); + QueryService queryService = Mockito.mock(QueryService.class); + ExecuteHistoryService historyService = + Mockito.mock(ExecuteHistoryService.class); + RuntimeException unsafe = new RuntimeException( + "No signature of method: secret.Groovy.stack()"); + Mockito.when(queryService.executeGremlinQuery( + Mockito.eq(client), Mockito.any(GremlinQuery.class))) + .thenThrow(unsafe); + + TestController controller = new TestController(client); + ReflectionTestUtils.setField(controller, "queryService", queryService); + ReflectionTestUtils.setField(controller, "historyService", historyService); + + Assert.assertThrows(RuntimeException.class, + () -> controller.gremlin("DEFAULT", "hugegraph", + new GremlinQuery("g.bad()"))); + + ArgumentCaptor saved = + ArgumentCaptor.forClass(ExecuteHistory.class); + Mockito.verify(historyService).update(saved.capture()); + Assert.assertEquals("GREMLIN_EXECUTION_FAILED", + saved.getValue().getFailureReason()); + Assert.assertFalse(saved.getValue().getFailureReason() + .contains("signature")); + } + + private static class TestController extends GremlinQueryController { + + private final HugeClient client; + + TestController(HugeClient client) { + this.client = client; + } + + @Override + protected HugeClient authGremlinClient(String graphSpace, String graph) { + return this.client; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GroovySchemaCompatibilityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GroovySchemaCompatibilityTest.java new file mode 100644 index 000000000..b1542498c --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GroovySchemaCompatibilityTest.java @@ -0,0 +1,131 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.apache.hugegraph.driver.SchemaManager; +import org.apache.hugegraph.service.schema.GroovySchemaCompatibility; +import org.apache.hugegraph.structure.schema.EdgeLabel; +import org.apache.hugegraph.structure.schema.IndexLabel; +import org.apache.hugegraph.structure.schema.PropertyKey; +import org.apache.hugegraph.structure.schema.VertexLabel; + +public class GroovySchemaCompatibilityTest { + + @Test + public void shouldPreserveLegacyGroovyResponse() { + SchemaManager schema = mock(SchemaManager.class); + when(schema.getGroovySchema()).thenReturn("legacy"); + + org.junit.Assert.assertEquals("legacy", + GroovySchemaCompatibility.export(schema)); + } + + @Test + public void shouldRebuildGroovyFromStructuredSchema() { + SchemaManager schema = mock(SchemaManager.class); + when(schema.getGroovySchema()).thenReturn(""); + + PropertyKey name = new PropertyKey.BuilderImpl("person's name", schema) + .asText().valueSet().ifNotExist().build(); + Map nested = new LinkedHashMap<>(); + nested.put("regions", Arrays.asList("east;west", "north\\south")); + nested.put("empty", Collections.emptyList()); + nested.put("emptyMap", Collections.emptyMap()); + name.userdata().put("constraints", nested); + name.userdata().put("codes", new int[]{1, 2}); + VertexLabel person = new VertexLabel.BuilderImpl("person", schema) + .usePrimaryKeyId() + .properties("person's name") + .primaryKeys("person's name") + .enableLabelIndex(false).ifNotExist().build(); + EdgeLabel knows = new EdgeLabel.BuilderImpl("knows", schema) + .link("person", "person") + .properties("person's name") + .multiTimes().ifNotExist().build(); + IndexLabel byName = new IndexLabel.BuilderImpl("by_name", schema) + .onV("person").by("person's name") + .secondary().ifNotExist().build(); + when(schema.getPropertyKeys()).thenReturn(Arrays.asList(name)); + when(schema.getVertexLabels()).thenReturn(Arrays.asList(person)); + when(schema.getEdgeLabels()).thenReturn(Arrays.asList(knows)); + when(schema.getIndexLabels()).thenReturn(Arrays.asList(byName)); + + String result = GroovySchemaCompatibility.export(schema); + + assertTrue(result.contains("propertyKey('person\\'s name').asText()")); + assertTrue(result.contains(".valueSet()")); + assertTrue(result.contains(".userdata('codes', [1, 2])")); + assertTrue(result.contains("'regions': ['east;west', " + + "'north\\\\south']")); + assertTrue(result.contains("'empty': []")); + assertTrue(result.contains("'emptyMap': [:]")); + assertTrue(result.contains("vertexLabel('person').usePrimaryKeyId()")); + assertTrue(result.contains(".primaryKeys('person\\'s name')")); + assertTrue(result.contains("edgeLabel('knows').link('person', 'person')")); + assertTrue(result.contains(".multiTimes()")); + assertTrue(result.contains("indexLabel('by_name').onV('person')")); + assertTrue(result.contains(".by('person\\'s name').secondary()")); + } + + @Test + public void shouldReturnEmptyForActuallyEmptyStructuredSchema() { + SchemaManager schema = mock(SchemaManager.class); + when(schema.getGroovySchema()).thenReturn(""); + when(schema.getPropertyKeys()).thenReturn(Collections.emptyList()); + when(schema.getVertexLabels()).thenReturn(Collections.emptyList()); + when(schema.getEdgeLabels()).thenReturn(Collections.emptyList()); + when(schema.getIndexLabels()).thenReturn(Collections.emptyList()); + + org.junit.Assert.assertEquals("", + GroovySchemaCompatibility.export(schema)); + } + + @Test + public void shouldSplitOnlyOutsideQuotedGroovyStrings() { + String content = "graph.schema().propertyKey('a;\\'b').userdata(" + + "'path', 'c:\\\\tmp;east\\nwest').create();\r\n" + + "graph.schema().propertyKey(\"second;key\").create();"; + + List statements = + GroovySchemaCompatibility.splitStatements(content); + + org.junit.Assert.assertEquals(2, statements.size()); + assertTrue(statements.get(0).contains("'a;\\'b'")); + assertTrue(statements.get(0).contains("c:\\\\tmp;east\\nwest")); + assertTrue(statements.get(1).contains("\"second;key\"")); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldRejectUnterminatedGroovyStrings() { + GroovySchemaCompatibility.splitStatements( + "graph.schema().propertyKey('unfinished);"); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/HubbleOptionsTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/HubbleOptionsTest.java new file mode 100644 index 000000000..453408899 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/HubbleOptionsTest.java @@ -0,0 +1,35 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import org.junit.Test; + +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.testutil.Assert; + +public class HubbleOptionsTest { + + @Test + public void testUploadFormatDefaultIncludesCsvAndTxt() { + Assert.assertTrue(HubbleOptions.UPLOAD_FILE_FORMAT_LIST.defaultValue() + .contains("csv")); + Assert.assertTrue(HubbleOptions.UPLOAD_FILE_FORMAT_LIST.defaultValue() + .contains("txt")); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JobManagerServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JobManagerServiceTest.java new file mode 100644 index 000000000..20439feae --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JobManagerServiceTest.java @@ -0,0 +1,132 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Arrays; + +import org.junit.Test; +import org.mockito.Mockito; + +import org.apache.hugegraph.entity.enums.JobStatus; +import org.apache.hugegraph.entity.enums.LoadStatus; +import org.apache.hugegraph.entity.load.JobManager; +import org.apache.hugegraph.entity.load.LoadTask; +import org.apache.hugegraph.mapper.load.JobManagerMapper; +import org.apache.hugegraph.service.load.JobManagerService; +import org.apache.hugegraph.service.load.LoadTaskService; +import org.apache.hugegraph.testutil.Assert; + +public class JobManagerServiceTest { + + @Test + public void testListByIdsAppliesGraphScope() throws Exception { + JobManagerService service = this.service(); + JobManagerMapper mapper = Mockito.mock(JobManagerMapper.class); + this.setField(service, "mapper", mapper); + + service.list("space-a", "graph-a", Arrays.asList(1, 2)); + + Mockito.verify(mapper).selectList(Mockito.any()); + Mockito.verify(mapper, Mockito.never()).selectBatchIds(Mockito.any()); + } + + @Test + public void testRefreshStatusMarksSucceededWhenAllLoadTasksSucceed() + throws Exception { + JobManagerService service = this.service(); + LoadTaskService taskService = Mockito.mock(LoadTaskService.class); + JobManagerMapper mapper = Mockito.mock(JobManagerMapper.class); + this.setField(service, "taskService", taskService); + this.setField(service, "mapper", mapper); + + JobManager job = JobManager.builder() + .id(1) + .jobStatus(JobStatus.LOADING) + .build(); + LoadTask task = LoadTask.builder().status(LoadStatus.SUCCEED).build(); + Mockito.when(taskService.taskListByJob(1)) + .thenReturn(Collections.singletonList(task)); + Mockito.when(mapper.updateById(job)).thenReturn(1); + + service.refreshStatus(job); + + Assert.assertEquals(JobStatus.SUCCESS, job.getJobStatus()); + Mockito.verify(mapper).updateById(job); + } + + @Test + public void testRefreshStatusMarksFailedWhenAnyLoadTaskFails() + throws Exception { + JobManagerService service = this.service(); + LoadTaskService taskService = Mockito.mock(LoadTaskService.class); + JobManagerMapper mapper = Mockito.mock(JobManagerMapper.class); + this.setField(service, "taskService", taskService); + this.setField(service, "mapper", mapper); + + JobManager job = JobManager.builder() + .id(1) + .jobStatus(JobStatus.LOADING) + .build(); + LoadTask task = LoadTask.builder().status(LoadStatus.FAILED).build(); + Mockito.when(taskService.taskListByJob(1)) + .thenReturn(Collections.singletonList(task)); + Mockito.when(mapper.updateById(job)).thenReturn(1); + + service.refreshStatus(job); + + Assert.assertEquals(JobStatus.FAILED, job.getJobStatus()); + Mockito.verify(mapper).updateById(job); + } + + @Test + public void testRefreshStatusKeepsLoadingWhenAnyLoadTaskRuns() + throws Exception { + JobManagerService service = this.service(); + LoadTaskService taskService = Mockito.mock(LoadTaskService.class); + JobManagerMapper mapper = Mockito.mock(JobManagerMapper.class); + this.setField(service, "taskService", taskService); + this.setField(service, "mapper", mapper); + + JobManager job = JobManager.builder() + .id(1) + .jobStatus(JobStatus.LOADING) + .build(); + LoadTask task = LoadTask.builder().status(LoadStatus.RUNNING).build(); + Mockito.when(taskService.taskListByJob(1)) + .thenReturn(Collections.singletonList(task)); + + service.refreshStatus(job); + + Assert.assertEquals(JobStatus.LOADING, job.getJobStatus()); + Mockito.verify(mapper, Mockito.never()).updateById(Mockito.any()); + } + + private JobManagerService service() { + return new JobManagerService(); + } + + private void setField(Object object, String name, Object value) + throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/K8sTokenEndpointSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/K8sTokenEndpointSecurityTest.java new file mode 100644 index 000000000..ffe4a08fe --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/K8sTokenEndpointSecurityTest.java @@ -0,0 +1,44 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Method; + +import org.apache.hugegraph.controller.op.K8sTokenController; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +public class K8sTokenEndpointSecurityTest { + + @Test + public void testK8sTokenEndpointHasNoWebMappings() { + Assert.assertNull(K8sTokenController.class.getDeclaredAnnotation( + RestController.class)); + Assert.assertNull(K8sTokenController.class.getDeclaredAnnotation( + RequestMapping.class)); + + for (Method method : K8sTokenController.class.getDeclaredMethods()) { + Assert.assertFalse(AnnotatedElementUtils.hasAnnotation( + method, RequestMapping.class)); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LegacyFacadeRemovalTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LegacyFacadeRemovalTest.java new file mode 100644 index 000000000..f8e234ca8 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LegacyFacadeRemovalTest.java @@ -0,0 +1,99 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.apache.hugegraph.controller.auth.RoleController; +import org.apache.hugegraph.controller.space.GraphSpaceController; +import org.apache.hugegraph.service.space.GraphSpaceService; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; + +import com.google.common.collect.ImmutableSet; + +public class LegacyFacadeRemovalTest { + + @Test + public void testLegacyDefaultFacadesAreRemoved() { + List violations = new ArrayList<>(); + this.findObsoleteRoutes(RoleController.class, + ImmutableSet.of("setdefaultrole", + "deldefaultrole"), + violations); + this.findObsoleteRoutes(GraphSpaceController.class, + ImmutableSet.of("setdefault", "getdefault"), + violations); + + Set obsoleteServiceMethods = + ImmutableSet.of("setdefault", "getdefault"); + Arrays.stream(GraphSpaceService.class.getMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) + .filter(method -> obsoleteServiceMethods.contains(method.getName())) + .forEach(method -> violations.add(GraphSpaceService.class.getSimpleName() + + "#" + method.getName())); + + Assert.assertTrue("Obsolete default facades remain: " + violations, + violations.isEmpty()); + } + + private void findObsoleteRoutes(Class controller, + Set obsoleteRoutes, + List violations) { + for (Method method : controller.getDeclaredMethods()) { + GetMapping get = method.getAnnotation(GetMapping.class); + if (get != null) { + this.findObsoleteValues(controller, method, get.value(), + obsoleteRoutes, violations); + this.findObsoleteValues(controller, method, get.path(), + obsoleteRoutes, violations); + } + DeleteMapping delete = method.getAnnotation(DeleteMapping.class); + if (delete != null) { + this.findObsoleteValues(controller, method, delete.value(), + obsoleteRoutes, violations); + this.findObsoleteValues(controller, method, delete.path(), + obsoleteRoutes, violations); + } + } + } + + private void findObsoleteValues(Class controller, Method method, + String[] mappings, + Set obsoleteRoutes, + List violations) { + Arrays.stream(mappings) + .filter(mapping -> obsoleteRoutes.contains(routeName(mapping))) + .forEach(mapping -> violations.add(controller.getSimpleName() + + "#" + method.getName() + + " -> " + mapping)); + } + + private static String routeName(String mapping) { + int separator = mapping.lastIndexOf('/'); + return mapping.substring(separator + 1); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java new file mode 100644 index 000000000..616c811c9 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java @@ -0,0 +1,170 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Method; +import java.lang.reflect.Field; +import java.util.Arrays; + +import org.junit.Test; + +import org.apache.hugegraph.entity.GraphConnection; +import org.apache.hugegraph.entity.load.FileMapping; +import org.apache.hugegraph.entity.load.LoadParameter; +import org.apache.hugegraph.loader.executor.LoadOptions; +import org.apache.hugegraph.service.load.LoadTaskService; +import org.apache.hugegraph.mapper.load.LoadTaskMapper; +import org.apache.hugegraph.testutil.Assert; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + +public class LoadTaskServiceTest { + + @Test + public void testListByIdsAppliesGraphScope() throws Exception { + LoadTaskService service = new LoadTaskService(); + LoadTaskMapper mapper = Mockito.mock(LoadTaskMapper.class); + Field field = LoadTaskService.class.getDeclaredField("mapper"); + field.setAccessible(true); + field.set(service, mapper); + + service.list("space-a", "graph-a", 7, Arrays.asList(1, 2)); + + ArgumentCaptor query = + ArgumentCaptor.forClass(QueryWrapper.class); + Mockito.verify(mapper).selectList(query.capture()); + Assert.assertContains("job_id", query.getValue().getSqlSegment()); + Mockito.verify(mapper, Mockito.never()).selectBatchIds(Mockito.any()); + } + + @Test + public void testLoadOptionsPreferBasicCredentialPassword() + throws Exception { + GraphConnection connection = this.connection("admin-pass", + "session-token"); + + LoadOptions options = this.buildLoadOptions(connection); + + Assert.assertEquals("admin", options.username); + Assert.assertEquals("admin-pass", options.password); + Assert.assertNull(options.token); + } + + @Test + public void testLoadOptionsUseTokenWithoutCredentialPassword() + throws Exception { + GraphConnection connection = this.connection(null, "session-token"); + + LoadOptions options = this.buildLoadOptions(connection); + + Assert.assertEquals("admin", options.username); + Assert.assertNull(options.password); + Assert.assertEquals("session-token", options.token); + } + + @Test + public void testLoadOptionsKeepPdRoutingWithoutHostPort() + throws Exception { + GraphConnection connection = this.pdConnection(); + + LoadOptions options = this.buildLoadOptions(connection); + + Assert.assertEquals("analytics", options.graphSpace); + Assert.assertEquals("hugegraph", options.graph); + Assert.assertEquals("127.0.0.1:8686", options.pdPeers); + Assert.assertEquals("cluster-a", options.cluster); + Assert.assertEquals("BOTH", options.routeType); + Assert.assertEquals("admin", options.username); + Assert.assertEquals("admin-pass", options.password); + Assert.assertNull(options.token); + Assert.assertEquals("localhost", options.host); + Assert.assertEquals(8080, options.port); + } + + @Test + public void testLoadOptionsPreferDirectServerOverPdPeers() + throws Exception { + GraphConnection connection = this.connection("admin-pass", + "session-token"); + connection.setCluster("cluster-a"); + connection.setRouteType("BOTH"); + connection.setPdPeers("127.0.0.1:8686"); + connection.setGraphSpace("DEFAULT"); + + LoadOptions options = this.buildLoadOptions(connection); + + Assert.assertNull(options.pdPeers); + Assert.assertEquals("127.0.0.1", options.host); + Assert.assertEquals(8080, options.port); + Assert.assertEquals("DEFAULT", options.graphSpace); + Assert.assertEquals("hugegraph", options.graph); + Assert.assertEquals("admin-pass", options.password); + } + + private LoadOptions buildLoadOptions(GraphConnection connection) + throws Exception { + LoadTaskService service = new LoadTaskService(); + Method method = LoadTaskService.class.getDeclaredMethod( + "buildLoadOptions", GraphConnection.class, FileMapping.class); + method.setAccessible(true); + return (LoadOptions) method.invoke(service, connection, + this.fileMapping()); + } + + private GraphConnection connection(String password, String token) { + GraphConnection connection = new GraphConnection(); + connection.setGraph("hugegraph"); + connection.setHost("127.0.0.1"); + connection.setPort(8080); + connection.setUsername("admin"); + connection.setPassword(password); + connection.setToken(token); + connection.setProtocol("http"); + return connection; + } + + private GraphConnection pdConnection() { + GraphConnection connection = new GraphConnection(); + connection.setCluster("cluster-a"); + connection.setRouteType("BOTH"); + connection.setPdPeers("127.0.0.1:8686"); + connection.setGraphSpace("analytics"); + connection.setGraph("hugegraph"); + connection.setUsername("admin"); + connection.setPassword("admin-pass"); + connection.setToken("session-token"); + return connection; + } + + private FileMapping fileMapping() { + FileMapping fileMapping = new FileMapping(); + fileMapping.setPath("/tmp/hubble-loader/edges.csv"); + fileMapping.setLoadParameter(LoadParameter.builder() + .checkVertex(false) + .insertTimeout(60) + .maxParseErrors(1) + .maxInsertErrors(1) + .retryTimes(1) + .retryInterval(1) + .build()); + return fileMapping; + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java new file mode 100644 index 000000000..2382e49ad --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java @@ -0,0 +1,89 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.Collections; +import java.util.Arrays; + +import org.apache.hugegraph.controller.load.FileMappingController; +import org.apache.hugegraph.controller.load.JobManagerController; +import org.apache.hugegraph.controller.load.LoadTaskController; +import org.apache.hugegraph.entity.load.FileMapping; +import org.apache.hugegraph.entity.load.JobManager; +import org.apache.hugegraph.entity.load.LoadTask; +import org.apache.hugegraph.service.load.FileMappingService; +import org.apache.hugegraph.service.load.JobManagerService; +import org.apache.hugegraph.service.load.LoadTaskService; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class LoaderScopeControllerTest { + + @Test + public void testJobLookupUsesGraphScope() { + JobManagerService service = Mockito.mock(JobManagerService.class); + JobManager expected = new JobManager(); + Mockito.when(service.get("space-a", "graph-a", 7)) + .thenReturn(expected); + + JobManagerController controller = new JobManagerController(service); + + org.junit.Assert.assertSame(expected, + controller.get("space-a", "graph-a", 7)); + } + + @Test + public void testFileClearUsesNestedScope() { + FileMappingService service = Mockito.mock(FileMappingService.class); + FileMapping mapping = FileMapping.builder().id(11).build(); + Mockito.when(service.listByJob("space-a", "graph-a", 7)) + .thenReturn(Collections.singletonList(mapping)); + FileMappingController controller = new FileMappingController(); + ReflectionTestUtils.setField(controller, "service", service); + + controller.clear("space-a", "graph-a", 7); + + Mockito.verify(service).remove(11); + Mockito.verify(service, Mockito.never()).listAll(); + } + + @Test + public void testLoadTaskLookupUsesNestedScope() { + LoadTaskService service = Mockito.mock(LoadTaskService.class); + LoadTask expected = new LoadTask(); + Mockito.when(service.get("space-a", "graph-a", 7, 13)) + .thenReturn(expected); + LoadTaskController controller = new LoadTaskController(service); + + org.junit.Assert.assertSame(expected, + controller.get("space-a", "graph-a", 7, + 13)); + } + + @Test + public void testLoadTaskBatchLookupUsesNestedScope() { + LoadTaskService service = Mockito.mock(LoadTaskService.class); + LoadTaskController controller = new LoadTaskController(service); + + controller.list("space-a", "graph-a", 7, Arrays.asList(13, 14)); + + Mockito.verify(service).list("space-a", "graph-a", 7, + Arrays.asList(13, 14)); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoginAttemptGuardTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoginAttemptGuardTest.java new file mode 100644 index 000000000..e72e4b695 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoginAttemptGuardTest.java @@ -0,0 +1,102 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.hugegraph.exception.LoginThrottledException; +import org.apache.hugegraph.service.auth.LoginAttemptGuard; +import org.junit.Assert; +import org.junit.Test; + +public class LoginAttemptGuardTest { + + @Test + public void testExponentialBackoffAndReset() { + AtomicLong now = new AtomicLong(1000L); + LoginAttemptGuard guard = new LoginAttemptGuard(4, 5000L, 600_000L, + 10_000L, now::get); + String username = "admin"; + String address = "127.0.0.1"; + + for (int i = 0; i < 3; i++) { + guard.recordFailure(username, address); + guard.checkAllowed(username, address); + } + + guard.recordFailure(username, address); + assertBlocked(guard, username, address); + + now.addAndGet(5000L); + guard.checkAllowed(username, address); + guard.recordFailure(username, address); + assertBlocked(guard, username, address); + + now.addAndGet(9999L); + assertBlocked(guard, username, address); + now.incrementAndGet(); + guard.checkAllowed(username, address); + + guard.reset(username, address); + guard.checkAllowed(username, address); + } + + @Test + public void testBackoffStopsAtConfiguredMaximum() { + AtomicLong now = new AtomicLong(); + LoginAttemptGuard guard = new LoginAttemptGuard(1, 5L, 20L, 100L, + now::get); + + guard.recordFailure("admin", "127.0.0.1"); + now.addAndGet(5L); + guard.recordFailure("admin", "127.0.0.1"); + now.addAndGet(10L); + guard.recordFailure("admin", "127.0.0.1"); + now.addAndGet(20L); + guard.recordFailure("admin", "127.0.0.1"); + + now.addAndGet(19L); + assertBlocked(guard, "admin", "127.0.0.1"); + now.incrementAndGet(); + guard.checkAllowed("admin", "127.0.0.1"); + } + + @Test + public void testConcurrentAttemptIsRejectedWithoutWaiting() { + LoginAttemptGuard guard = new LoginAttemptGuard(4, 5000L, 600_000L, + 10_000L, + System::currentTimeMillis); + + guard.checkAllowed("admin", "127.0.0.1"); + assertBlocked(guard, "admin", "127.0.0.1"); + + guard.release("admin", "127.0.0.1"); + guard.checkAllowed("admin", "127.0.0.1"); + } + + private static void assertBlocked(LoginAttemptGuard guard, String username, + String address) { + try { + guard.checkAllowed(username, address); + Assert.fail("Expected login attempt to be throttled"); + } catch (LoginThrottledException e) { + Assert.assertTrue(e.getRetrySeconds() > 0L); + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/MessageSourceHandlerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/MessageSourceHandlerTest.java new file mode 100644 index 000000000..c17ef4524 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/MessageSourceHandlerTest.java @@ -0,0 +1,128 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.io.InputStream; +import java.util.Locale; +import java.util.Properties; + +import javax.servlet.http.Cookie; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.entity.UserInfo; +import org.apache.hugegraph.handler.MessageSourceHandler; +import org.apache.hugegraph.service.UserInfoService; + +public class MessageSourceHandlerTest { + + @After + public void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void testRequestLanguageOverridesStoredUserLocale() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest( + "POST", "/api/v1.3/gremlin-query"); + request.addHeader("Accept-Language", "en-US"); + request.setCookies(new Cookie(Constant.COOKIE_USER, "admin")); + RequestContextHolder.setRequestAttributes( + new ServletRequestAttributes(request)); + + StaticMessageSource messageSource = new StaticMessageSource(); + messageSource.addMessage("gremlin.execute.failed", Locale.US, + "Gremlin execute failed"); + messageSource.addMessage("gremlin.execute.failed", + Locale.SIMPLIFIED_CHINESE, + "Gremlin 执行失败"); + + UserInfoService service = Mockito.mock(UserInfoService.class); + Mockito.when(service.getByName("admin")) + .thenReturn(UserInfo.builder() + .username("admin") + .locale("zh_CN") + .build()); + + MessageSourceHandler handler = new MessageSourceHandler(); + this.setField(handler, "messageSource", messageSource); + this.setField(handler, "request", request); + this.setField(handler, "service", service); + + Assert.assertEquals("Gremlin execute failed", + handler.getMessage("gremlin.execute.failed")); + } + + @Test + public void testEnglishMessagesDoNotFallbackToSystemLocale() + throws Exception { + Locale previousDefault = Locale.getDefault(); + Locale.setDefault(Locale.SIMPLIFIED_CHINESE); + try { + Properties properties = new Properties(); + InputStream stream = this.getClass().getClassLoader() + .getResourceAsStream( + "application.properties"); + Assert.assertNotNull(stream); + properties.load(stream); + + Assert.assertEquals("false", + properties.getProperty( + "spring.messages." + + "fallback-to-system-locale")); + + ResourceBundleMessageSource messageSource = + new ResourceBundleMessageSource(); + messageSource.setBasename(properties.getProperty( + "spring.messages.basename")); + messageSource.setDefaultEncoding(properties.getProperty( + "spring.messages.encoding")); + messageSource.setFallbackToSystemLocale(Boolean.parseBoolean( + properties.getProperty("spring.messages." + + "fallback-to-system-locale"))); + + String message = messageSource.getMessage("gremlin.execute.failed", + new Object[]{"bad"}, + Locale.US); + + Assert.assertEquals("Gremlin execute failed, the details: bad", + message); + } finally { + Locale.setDefault(previousDefault); + } + } + + private void setField(Object object, String name, Object value) + throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoControllerTest.java new file mode 100644 index 000000000..828ab971c --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoControllerTest.java @@ -0,0 +1,117 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.web.bind.annotation.PostMapping; + +import org.apache.hugegraph.controller.algorithm.OltpAlgoController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.algorithm.ShortestPathEntity; +import org.apache.hugegraph.entity.query.GremlinResult; +import org.apache.hugegraph.service.algorithm.OltpAlgoService; +import org.apache.hugegraph.testutil.Assert; + +public class OltpAlgoControllerTest { + + @Test + public void testShortestPathUsesGremlinCapableClient() throws Exception { + HugeClient tokenClient = Mockito.mock(HugeClient.class); + HugeClient gremlinClient = Mockito.mock(HugeClient.class); + ShortestPathEntity body = new ShortestPathEntity(); + GremlinResult result = GremlinResult.builder().build(); + OltpAlgoService service = Mockito.mock(OltpAlgoService.class); + Mockito.when(service.shortestPath(gremlinClient, body)) + .thenReturn(result); + + TestOltpAlgoController controller = new TestOltpAlgoController(); + controller.authClient = tokenClient; + controller.gremlinClient = gremlinClient; + this.setService(controller, service); + + GremlinResult actual = controller.shortPath("DEFAULT", "hugegraph", body); + + Assert.assertSame(result, actual); + Assert.assertTrue(controller.gremlinClientCreated); + Assert.assertFalse(controller.authClientCreated); + Assert.assertEquals("DEFAULT", controller.graphSpace); + Assert.assertEquals("hugegraph", controller.graph); + Mockito.verify(service).shortestPath(gremlinClient, body); + Mockito.verifyZeroInteractions(tokenClient); + } + + @Test + public void testShortPathAliasMappingExists() throws Exception { + Method method = OltpAlgoController.class.getDeclaredMethod("shortPathAlias", + String.class, + String.class, + org.apache.hugegraph.entity.algorithm.ShortestPathEntity.class); + + PostMapping mapping = method.getAnnotation(PostMapping.class); + Assert.assertEquals("shortpath", mapping.value()[0]); + } + + @Test + public void testAllShortPathAliasMappingExists() throws Exception { + Method method = OltpAlgoController.class.getDeclaredMethod("allShortPathAlias", + String.class, + String.class, + org.apache.hugegraph.entity.algorithm.AllShortestPathsEntity.class); + + PostMapping mapping = method.getAnnotation(PostMapping.class); + Assert.assertEquals("allshortpath", mapping.value()[0]); + } + + private void setService(OltpAlgoController controller, + OltpAlgoService service) throws Exception { + Field field = OltpAlgoController.class.getDeclaredField("service"); + field.setAccessible(true); + field.set(controller, service); + } + + private static class TestOltpAlgoController extends OltpAlgoController { + + private HugeClient authClient; + private HugeClient gremlinClient; + private boolean authClientCreated; + private boolean gremlinClientCreated; + private String graphSpace; + private String graph; + + @Override + protected HugeClient authClient(String graphSpace, String graph) { + this.authClientCreated = true; + this.graphSpace = graphSpace; + this.graph = graph; + return this.authClient; + } + + @Override + protected HugeClient authGremlinClient(String graphSpace, String graph) { + this.gremlinClientCreated = true; + this.graphSpace = graphSpace; + this.graph = graph; + return this.gremlinClient; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoServiceTest.java new file mode 100644 index 000000000..78c4f31da --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoServiceTest.java @@ -0,0 +1,266 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.junit.Test; +import org.mockito.Mockito; + +import org.apache.hugegraph.api.gremlin.GremlinRequest; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.GraphManager; +import org.apache.hugegraph.driver.GremlinManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.TraverserManager; +import org.apache.hugegraph.entity.algorithm.WeightedShortestPathEntity; +import org.apache.hugegraph.entity.query.GraphView; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.query.ExecuteHistoryService; +import org.apache.hugegraph.service.algorithm.OltpAlgoService; +import org.apache.hugegraph.structure.graph.Edge; +import org.apache.hugegraph.structure.graph.Path; +import org.apache.hugegraph.structure.graph.Vertex; +import org.apache.hugegraph.structure.gremlin.ResultSet; +import org.apache.hugegraph.structure.traverser.WeightedPath; +import org.apache.hugegraph.testutil.Assert; + +public class OltpAlgoServiceTest { + + @Test + public void testBuildPathGraphViewKeepsPathEdges() throws Exception { + Vertex marko = new Vertex("person"); + marko.id("marko"); + Vertex vadas = new Vertex("person"); + vadas.id("vadas"); + + Edge knows = new Edge("knows"); + knows.id("S1:marko>vadas"); + knows.source(marko); + knows.target(vadas); + + HugeClient client = Mockito.mock(HugeClient.class); + Path path = new Path(Arrays.asList(marko, knows, vadas)); + + GraphView graphView = this.buildPathGraphView(client, path); + + Assert.assertEquals(2, graphView.getVertices().size()); + Assert.assertEquals(1, graphView.getEdges().size()); + Assert.assertTrue(graphView.getEdges().contains(knows)); + } + + @Test + public void testBuildPathGraphViewBackfillsEdgeEndpointVertices() + throws Exception { + Vertex marko = new Vertex("person"); + marko.id("marko"); + Vertex vadas = new Vertex("person"); + vadas.id("vadas"); + + Edge knows = new Edge("knows"); + knows.id("S1:marko>vadas"); + knows.source(marko); + knows.target(vadas); + + GremlinManager gremlin = Mockito.mock(GremlinManager.class); + Mockito.when(gremlin.gremlin(Mockito.anyString())) + .thenAnswer(invocation -> new GremlinRequest.Builder( + invocation.getArgument(0), gremlin)); + Mockito.when(gremlin.execute(Mockito.any())) + .thenReturn(this.resultSet(marko, vadas)); + HugeClient client = Mockito.mock(HugeClient.class); + Mockito.when(client.gremlin()).thenReturn(gremlin); + Path path = new Path(Arrays.asList(knows)); + + GraphView graphView = this.buildPathGraphView(client, path); + + Assert.assertEquals(2, graphView.getVertices().size()); + Assert.assertEquals(1, graphView.getEdges().size()); + Assert.assertTrue(graphView.getEdges().contains(knows)); + Mockito.verify(gremlin, Mockito.times(1)) + .gremlin("g.V('marko','vadas').limit(1000)"); + Mockito.verify(client, Mockito.never()).graph(); + } + + @Test + public void testBuildPathGraphViewBackfillsEdgesForVertexIdPath() + throws Exception { + Vertex marko = new Vertex("person"); + marko.id("marko"); + Vertex vadas = new Vertex("person"); + vadas.id("vadas"); + + Edge knows = new Edge("knows"); + knows.id("S1:marko>vadas"); + knows.source(marko); + knows.target(vadas); + + GremlinManager gremlin = Mockito.mock(GremlinManager.class); + Mockito.when(gremlin.gremlin(Mockito.anyString())) + .thenAnswer(invocation -> new GremlinRequest.Builder( + invocation.getArgument(0), gremlin)); + Mockito.when(gremlin.execute(Mockito.any())) + .thenReturn(this.resultSet(marko, vadas), + this.resultSet(knows)); + HugeClient client = Mockito.mock(HugeClient.class); + Mockito.when(client.gremlin()).thenReturn(gremlin); + OltpAlgoService service = this.serviceWithConfig(); + Path path = new Path(Arrays.asList("marko", "vadas")); + + GraphView graphView = this.buildPathGraphView(service, client, path); + + Assert.assertEquals(2, graphView.getVertices().size()); + Assert.assertEquals(1, graphView.getEdges().size()); + Assert.assertTrue(graphView.getEdges().contains(knows)); + Mockito.verify(gremlin).gremlin("g.V('marko','vadas').limit(1000)"); + Mockito.verify(gremlin).gremlin( + "g.V('marko','vadas').bothE().local(limit(1000)).dedup()"); + } + + @Test + public void testBuildPathGraphViewIgnoresNullVertexIds() + throws Exception { + HugeClient client = Mockito.mock(HugeClient.class); + OltpAlgoService service = this.serviceWithConfig(); + Path path = new Path(Arrays.asList((Object) null)); + + GraphView graphView = this.buildPathGraphView(service, client, path); + + Assert.assertEquals(0, graphView.getVertices().size()); + Assert.assertEquals(0, graphView.getEdges().size()); + Mockito.verify(client, Mockito.never()).gremlin(); + } + + @Test + public void testWeightedShortestPathUsesReturnedPathEdges() + throws Exception { + Vertex marko = new Vertex("person"); + marko.id("marko"); + Vertex vadas = new Vertex("person"); + vadas.id("vadas"); + + Edge pathEdge = new Edge("knows"); + pathEdge.id("S1:marko>vadas"); + pathEdge.source(marko); + pathEdge.target(vadas); + + Edge nonPathEdge = new Edge("created"); + nonPathEdge.id("S2:marko>vadas"); + nonPathEdge.source(marko); + nonPathEdge.target(vadas); + + WeightedPath path = new WeightedPath(); + this.setField(path, "vertices", new LinkedHashSet<>( + Arrays.asList(marko, vadas))); + this.setField(path, "edges", new LinkedHashSet<>( + Arrays.asList(pathEdge))); + + TraverserManager traverser = Mockito.mock(TraverserManager.class); + Mockito.when(traverser.weightedShortestPath( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyLong(), Mockito.eq(true), Mockito.eq(true))) + .thenReturn(path); + Mockito.when(traverser.weightedShortestPath( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyLong(), Mockito.eq(true), Mockito.eq(false))) + .thenReturn(path); + + GremlinManager gremlin = Mockito.mock(GremlinManager.class); + Mockito.when(gremlin.gremlin(Mockito.anyString())) + .thenAnswer(invocation -> new GremlinRequest.Builder( + invocation.getArgument(0), gremlin)); + Mockito.when(gremlin.execute(Mockito.any())) + .thenReturn(this.resultSet(pathEdge, nonPathEdge)); + + HugeClient client = Mockito.mock(HugeClient.class); + Mockito.when(client.getGraphSpaceName()).thenReturn("DEFAULT"); + Mockito.when(client.getGraphName()).thenReturn("hugegraph"); + Mockito.when(client.traverser()).thenReturn(traverser); + Mockito.when(client.gremlin()).thenReturn(gremlin); + + OltpAlgoService service = this.serviceWithConfig(); + this.setField(service, "historyService", + Mockito.mock(ExecuteHistoryService.class)); + + WeightedShortestPathEntity body = WeightedShortestPathEntity.builder() + .source("marko") + .target("vadas") + .weight("weight") + .build(); + GraphView graphView = service.weightedShortestPath(client, body) + .getGraphView(); + + Assert.assertEquals(2, graphView.getVertices().size()); + Assert.assertEquals(1, graphView.getEdges().size()); + Assert.assertTrue(graphView.getEdges().contains(pathEdge)); + Assert.assertFalse(graphView.getEdges().contains(nonPathEdge)); + Mockito.verify(traverser).weightedShortestPath( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyLong(), Mockito.eq(true), Mockito.eq(true)); + Mockito.verify(gremlin, Mockito.never()).gremlin(Mockito.contains("bothE")); + } + + private GraphView buildPathGraphView(HugeClient client, Path path) + throws Exception { + return this.buildPathGraphView(new OltpAlgoService(), client, path); + } + + private GraphView buildPathGraphView(OltpAlgoService service, + HugeClient client, Path path) + throws Exception { + Method method = OltpAlgoService.class.getDeclaredMethod("buildPathGraphView", + HugeClient.class, + Path.class); + method.setAccessible(true); + return (GraphView) method.invoke(service, client, path); + } + + private OltpAlgoService serviceWithConfig() throws Exception { + OltpAlgoService service = new OltpAlgoService(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS)) + .thenReturn(1000); + Mockito.when(config.get(HubbleOptions.GREMLIN_EDGES_TOTAL_LIMIT)) + .thenReturn(1000); + Mockito.when(config.get(HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT)) + .thenReturn(1000); + this.setField(service, "config", config); + return service; + } + + private ResultSet resultSet(Object... data) throws Exception { + ResultSet resultSet = new ResultSet(); + this.setField(resultSet, "data", Arrays.asList(data)); + resultSet.graphManager(Mockito.mock(GraphManager.class)); + return resultSet; + } + + private void setField(Object object, String name, Object value) + throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/PriorityFixTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/PriorityFixTest.java new file mode 100644 index 000000000..5581c410a --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/PriorityFixTest.java @@ -0,0 +1,94 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.util.Arrays; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.handler.CustomInterceptor; +import org.apache.hugegraph.util.PageUtil; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.baomidou.mybatisplus.core.metadata.IPage; + +public class PriorityFixTest { + + @Test + public void testAfterCompletionClosesRequestClient() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + HugeClient client = Mockito.mock(HugeClient.class); + request.setAttribute("hugeClient", client); + + new CustomInterceptor().afterCompletion(request, + new MockHttpServletResponse(), + new Object(), null); + + Mockito.verify(client).close(); + Assert.assertNull(request.getAttribute("hugeClient")); + } + + @Test + public void testNewPageCalculatesPageCount() { + IPage page = PageUtil.newPage(Arrays.asList(3, 4), 2, 2, 5); + + Assert.assertEquals(3L, page.getPages()); + Assert.assertEquals(5L, page.getTotal()); + Assert.assertEquals(Arrays.asList(3, 4), page.getRecords()); + } + + @Test + public void testPageKeepsLegacyAllPageSentinel() { + IPage page = PageUtil.page(Arrays.asList(1, 2, 3), 1, -1); + + Assert.assertEquals(Arrays.asList(1, 2, 3), page.getRecords()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNewPageRejectsZeroPageSize() { + PageUtil.newPage(Arrays.asList(), 1, 0, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testPositivePageRejectsAllPageSentinel() { + PageUtil.checkPositivePage(1, -1); + } + + @Test(expected = ExternalException.class) + public void testInterceptorRejectsOversizedPage() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/list"); + request.setParameter("page_size", "501"); + + new CustomInterceptor().preHandle(request, + new MockHttpServletResponse(), + new Object()); + } + + @Test + public void testInterceptorKeepsLegacyAllPageSentinel() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/list"); + request.setParameter("page_size", "-1"); + + Assert.assertTrue(new CustomInterceptor().preHandle( + request, new MockHttpServletResponse(), new Object())); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/QueryServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/QueryServiceTest.java new file mode 100644 index 000000000..48f262a47 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/QueryServiceTest.java @@ -0,0 +1,171 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.mockito.Mockito; + +import org.apache.hugegraph.api.gremlin.GremlinRequest; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.GraphManager; +import org.apache.hugegraph.driver.GremlinManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.query.AdjacentQuery; +import org.apache.hugegraph.entity.schema.VertexLabelEntity; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.query.QueryService; +import org.apache.hugegraph.service.schema.VertexLabelService; +import org.apache.hugegraph.structure.constant.Direction; +import org.apache.hugegraph.structure.constant.IdStrategy; +import org.apache.hugegraph.structure.gremlin.ResultSet; +import org.apache.hugegraph.testutil.Assert; + +public class QueryServiceTest { + + @Test + public void testExpandVertexEscapesGremlinLiterals() throws Exception { + GremlinManager gremlin = this.mockGremlin(); + HugeClient client = this.mockClient(gremlin); + QueryService service = this.serviceWithConfig(); + + AdjacentQuery query = AdjacentQuery.builder() + .vertexId("marko'id") + .vertexLabel("person") + .edgeLabel("edge'label\\x\r\n") + .direction(Direction.BOTH) + .conditions(Collections.singletonList( + AdjacentQuery.Condition + .builder() + .key("name'key\r") + .operator("eq") + .value("value'quote\\slash\r\n") + .build())) + .build(); + + service.expandVertex(client, query); + + Mockito.verify(gremlin).gremlin( + "g.V('marko\\'id').toE(BOTH, 'edge\\'label\\\\x\\r\\n')" + + ".has('name\\'key\\r', eq('value\\'quote\\\\slash\\r\\n'))" + + ".limit(100).otherV().path()"); + } + + @Test + public void testExpandVertexRejectsInvalidOperatorInService() + throws Exception { + GremlinManager gremlin = this.mockGremlin(); + HugeClient client = this.mockClient(gremlin); + QueryService service = this.serviceWithConfig(); + + AdjacentQuery query = AdjacentQuery.builder() + .vertexId("marko") + .vertexLabel("person") + .conditions(Collections.singletonList( + AdjacentQuery.Condition + .builder() + .key("name") + .operator("drop") + .value("value") + .build())) + .build(); + + Assert.assertThrows(ExternalException.class, () -> { + service.expandVertex(client, query); + }); + Mockito.verify(gremlin, Mockito.never()).gremlin(Mockito.anyString()); + } + + @Test + public void testExpandVertexRejectsStructuredConditionValue() + throws Exception { + GremlinManager gremlin = this.mockGremlin(); + HugeClient client = this.mockClient(gremlin); + QueryService service = this.serviceWithConfig(); + + AdjacentQuery query = AdjacentQuery.builder() + .vertexId("marko") + .vertexLabel("person") + .conditions(Collections.singletonList( + AdjacentQuery.Condition + .builder() + .key("name") + .operator("eq") + .value(Collections.singletonMap( + "payload", + "x'));g.V().drop();//")) + .build())) + .build(); + + Assert.assertThrows(ExternalException.class, () -> { + service.expandVertex(client, query); + }); + Mockito.verify(gremlin, Mockito.never()).gremlin(Mockito.anyString()); + } + + private QueryService serviceWithConfig() throws Exception { + QueryService service = new QueryService(); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT)) + .thenReturn(100); + this.setField(service, "config", config); + VertexLabelService vlService = Mockito.mock(VertexLabelService.class); + Mockito.when(vlService.get(Mockito.eq("person"), Mockito.any())) + .thenReturn(VertexLabelEntity.builder() + .name("person") + .idStrategy(IdStrategy.CUSTOMIZE_STRING) + .build()); + this.setField(service, "vlService", vlService); + return service; + } + + private HugeClient mockClient(GremlinManager gremlin) { + HugeClient client = Mockito.mock(HugeClient.class); + Mockito.when(client.gremlin()).thenReturn(gremlin); + return client; + } + + private GremlinManager mockGremlin() throws Exception { + GremlinManager gremlin = Mockito.mock(GremlinManager.class); + Mockito.when(gremlin.gremlin(Mockito.anyString())) + .thenAnswer(invocation -> new GremlinRequest.Builder( + invocation.getArgument(0), gremlin)); + Mockito.when(gremlin.execute(Mockito.any())) + .thenReturn(this.resultSet()); + return gremlin; + } + + private ResultSet resultSet() throws Exception { + ResultSet resultSet = new ResultSet(); + this.setField(resultSet, "data", Arrays.asList()); + resultSet.graphManager(Mockito.mock(GraphManager.class)); + return resultSet; + } + + private void setField(Object object, String name, Object value) + throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/SchemaServiceViewTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/SchemaServiceViewTest.java new file mode 100644 index 000000000..9a11f67c0 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/SchemaServiceViewTest.java @@ -0,0 +1,125 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.schema.Property; +import org.apache.hugegraph.entity.schema.PropertyKeyEntity; +import org.apache.hugegraph.entity.schema.VertexLabelEntity; +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.service.schema.EdgeLabelService; +import org.apache.hugegraph.service.schema.PropertyKeyService; +import org.apache.hugegraph.service.schema.SchemaService; +import org.apache.hugegraph.service.schema.VertexLabelService; +import org.apache.hugegraph.structure.constant.Cardinality; +import org.apache.hugegraph.structure.constant.DataType; +import org.apache.hugegraph.structure.constant.IdStrategy; + +public class SchemaServiceViewTest { + + @Test + public void testGetSchemaViewReturnsVertexPropertyTypes() throws Exception { + PropertyKeyEntity name = PropertyKeyEntity.builder() + .name("name") + .dataType(DataType.TEXT) + .cardinality(Cardinality.SINGLE) + .build(); + VertexLabelEntity person = VertexLabelEntity.builder() + .name("person") + .idStrategy(IdStrategy.PRIMARY_KEY) + .primaryKeys(Collections.singletonList( + "name")) + .properties(Collections.singleton( + new Property( + "name", false, + DataType.TEXT, + Cardinality.SINGLE))) + .build(); + SchemaService service = service(Collections.singletonList(name), + Collections.singletonList(person)); + + SchemaService.SchemaView view = service.getSchemaView( + Mockito.mock(HugeClient.class)); + + Assert.assertEquals(1, view.getVertices().size()); + Map vertex = view.getVertices().get(0); + Assert.assertEquals("person", vertex.get("id")); + Assert.assertEquals(Collections.singletonList("name"), + vertex.get("primary_keys")); + Assert.assertEquals(Collections.singletonMap("name", "text"), + vertex.get("properties")); + Assert.assertTrue(view.getEdges().isEmpty()); + } + + @Test + public void testGetSchemaViewRejectsMissingPropertyKey() throws Exception { + VertexLabelEntity person = VertexLabelEntity.builder() + .name("person") + .idStrategy(IdStrategy.DEFAULT) + .properties(Collections.singleton( + new Property( + "missing", true, + DataType.TEXT, + Cardinality.SINGLE))) + .build(); + SchemaService service = service(Collections.emptyList(), + Collections.singletonList(person)); + + org.apache.hugegraph.testutil.Assert.assertThrows( + InternalException.class, () -> { + service.getSchemaView(Mockito.mock(HugeClient.class)); + }); + } + + private static SchemaService service(List propertyKeys, + List vertexLabels) + throws Exception { + PropertyKeyService pkService = Mockito.mock(PropertyKeyService.class); + VertexLabelService vlService = Mockito.mock(VertexLabelService.class); + EdgeLabelService elService = Mockito.mock(EdgeLabelService.class); + Mockito.when(pkService.list(Mockito.any(HugeClient.class))) + .thenReturn(propertyKeys); + Mockito.when(vlService.list(Mockito.any(HugeClient.class))) + .thenReturn(vertexLabels); + Mockito.when(elService.list(Mockito.any(HugeClient.class))) + .thenReturn(Collections.emptyList()); + + SchemaService service = new SchemaService(); + inject(service, "pkService", pkService); + inject(service, "vlService", vlService); + inject(service, "elService", elService); + return service; + } + + private static void inject(Object target, String name, Object value) + throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/SessionTimeoutConfigTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/SessionTimeoutConfigTest.java new file mode 100644 index 000000000..464cb3553 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/SessionTimeoutConfigTest.java @@ -0,0 +1,53 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Test; + +public class SessionTimeoutConfigTest { + + @Test + public void testSessionIdleTimeoutIsExplicitlyFortyEightHours() + throws IOException { + Enumeration resources = this.getClass().getClassLoader() + .getResources( + "application.properties"); + while (resources.hasMoreElements()) { + Properties properties = new Properties(); + try (InputStream input = resources.nextElement().openStream()) { + properties.load(input); + } + if (!"hugegraph-hubble".equals( + properties.getProperty("info.app.name"))) { + continue; + } + Assert.assertEquals("48h", properties.getProperty( + "server.servlet.session.timeout")); + return; + } + Assert.fail("Hubble application.properties was not found"); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java index 6cdd3026a..58d819f04 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -18,14 +18,45 @@ package org.apache.hugegraph.unit; +import org.apache.hugegraph.controller.ingest.IngestControllerTest; +import org.apache.hugegraph.controller.langchain.LangChainControllerSecurityTest; +import org.apache.hugegraph.controller.schema.SchemaControllerSecurityTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ - EntityUtilTest.class, - FileUtilTest.class + AuthSecurityTest.class, + AppTypeTest.class, + AuthzRouteRegistrationTest.class, + BusinessAssertTest.class, + BaseControllerGremlinClientTest.class, + ConsolePrintTest.class, + EmptyCatchTest.class, + FileMappingSchemaTest.class, + FileUploadControllerTest.class, + FileUtilTest.class, + GraphServiceImportTest.class, + GraphMetricsControllerTest.class, + GraphsControllerCanonicalTest.class, + GremlinHistoryFailureTest.class, + HubbleOptionsTest.class, + IngestControllerTest.class, + LangChainControllerSecurityTest.class, + LegacyFacadeRemovalTest.class, + MessageSourceHandlerTest.class, + SchemaControllerSecurityTest.class, + GroovySchemaCompatibilityTest.class, + JobManagerServiceTest.class, + K8sTokenEndpointSecurityTest.class, + LoadTaskServiceTest.class, + LoaderScopeControllerTest.class, + LoginAttemptGuardTest.class, + OltpAlgoControllerTest.class, + OltpAlgoServiceTest.class, + PriorityFixTest.class, + QueryServiceTest.class, + UrlUtilTest.class }) public class UnitTestSuite { - } diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UrlUtilTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UrlUtilTest.java new file mode 100644 index 000000000..3e8a54752 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UrlUtilTest.java @@ -0,0 +1,88 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import org.junit.Test; + +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.UrlUtil; + +public class UrlUtilTest { + + @Test + public void testParseHostIgnoresPathAndQuery() { + UrlUtil.Host host = UrlUtil.parseHost( + "http://127.0.0.1:8080/graphs/hugegraph?debug=true"); + + Assert.assertEquals("http", host.getScheme()); + Assert.assertEquals("127.0.0.1", host.getHost()); + Assert.assertEquals(8080, host.getPort()); + } + + @Test + public void testParseHostUsesSchemeDefaultPort() { + UrlUtil.Host http = UrlUtil.parseHost("http://localhost"); + Assert.assertEquals("http", http.getScheme()); + Assert.assertEquals("localhost", http.getHost()); + Assert.assertEquals(80, http.getPort()); + + UrlUtil.Host https = UrlUtil.parseHost("https://example.com/base"); + Assert.assertEquals("https", https.getScheme()); + Assert.assertEquals("example.com", https.getHost()); + Assert.assertEquals(443, https.getPort()); + } + + @Test + public void testParseHostHandlesIpv6Literal() { + UrlUtil.Host host = UrlUtil.parseHost("http://[::1]:8080/path"); + + Assert.assertEquals("http", host.getScheme()); + Assert.assertEquals("::1", host.getHost()); + Assert.assertEquals(8080, host.getPort()); + } + + @Test + public void testParseHostKeepsHostPortWithoutScheme() { + UrlUtil.Host host = UrlUtil.parseHost("127.0.0.1:8080"); + + Assert.assertEquals(null, host.getScheme()); + Assert.assertEquals("127.0.0.1", host.getHost()); + Assert.assertEquals(8080, host.getPort()); + } + + @Test + public void testParseHostWithoutSchemeAllowsAbsoluteUrlInPath() { + UrlUtil.Host host = UrlUtil.parseHost( + "127.0.0.1:8080/path?next=http://example.com"); + + Assert.assertEquals(null, host.getScheme()); + Assert.assertEquals("127.0.0.1", host.getHost()); + Assert.assertEquals(8080, host.getPort()); + } + + @Test + public void testParseHostRejectsEmptyExplicitPort() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + UrlUtil.parseHost("http://localhost:"); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + UrlUtil.parseHost("https://localhost:/path"); + }); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserApiContractTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserApiContractTest.java new file mode 100644 index 000000000..19c40f5f2 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserApiContractTest.java @@ -0,0 +1,139 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.lang.reflect.Field; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.util.NestedServletException; + +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.controller.auth.UserController; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.exception.ExternalException; +import org.apache.hugegraph.exception.UnauthorizedException; +import org.apache.hugegraph.service.auth.UserService; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class UserApiContractTest { + + private MockMvc mvc; + private HugeClient client; + private UserService userService; + private RequestPostProcessor withClient; + + @Before + public void setup() throws Exception { + UserController controller = new UserController(); + this.userService = Mockito.mock(UserService.class); + this.setField(controller, "userService", this.userService); + this.client = Mockito.mock(HugeClient.class); + this.withClient = request -> { + request.setAttribute("hugeClient", this.client); + return request; + }; + this.mvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + public void testUpdatePersonalUsesPutAndJsonBody() throws Exception { + MockHttpSession session = new MockHttpSession(); + session.setAttribute(Constant.USERNAME_KEY, "alice"); + + this.mvc.perform(put("/api/v1.3/auth/users/personal") + .with(this.withClient) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"nickname\":\"Alice\"," + + "\"description\":\"owner\"}")) + .andExpect(status().isOk()); + + Mockito.verify(this.userService) + .updatePersonal(this.client, "alice", "Alice", "owner"); + } + + @Test + public void testUpdatePersonalRequiresAuthentication() throws Exception { + this.assertRequestCause(UnauthorizedException.class, + () -> this.mvc.perform( + put("/api/v1.3/auth/users/personal") + .with(this.withClient) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"nickname\":\"Alice\"}"))); + } + + @Test + public void testUpdatePersonalRejectsEmptyNickname() throws Exception { + MockHttpSession session = new MockHttpSession(); + session.setAttribute(Constant.USERNAME_KEY, "alice"); + + this.assertRequestCause(ExternalException.class, + () -> this.mvc.perform( + put("/api/v1.3/auth/users/personal") + .with(this.withClient) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"nickname\":\"\"}"))); + } + + @Test + public void testRetiredUserRoutesAreUnavailable() throws Exception { + this.mvc.perform(post("/api/v1.3/auth/users/uuap")) + .andExpect(status().isMethodNotAllowed()); + this.mvc.perform(delete("/api/v1.3/auth/users/super/alice")) + .andExpect(status().isNotFound()); + Mockito.verifyZeroInteractions(this.userService); + } + + private void assertRequestCause(Class expected, + Request request) throws Exception { + try { + request.perform(); + org.junit.Assert.fail("Expected request to throw " + + expected.getName()); + } catch (NestedServletException e) { + org.apache.hugegraph.testutil.Assert.assertInstanceOf(expected, + e.getCause()); + } + } + + @FunctionalInterface + private interface Request { + + void perform() throws Exception; + } + + private void setField(Object target, String name, Object value) + throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserImportSafetyTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserImportSafetyTest.java new file mode 100644 index 000000000..4bac0b1b2 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserImportSafetyTest.java @@ -0,0 +1,96 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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 org.apache.hugegraph.unit; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.web.multipart.MultipartFile; + +import org.apache.hugegraph.exception.InternalException; +import org.apache.hugegraph.service.auth.UserService; + +public class UserImportSafetyTest { + + @Test + public void testTemporaryFileFailureIsNotReturnedAsNull() throws Exception { + MultipartFile upload = Mockito.mock(MultipartFile.class); + Mockito.when(upload.getOriginalFilename()).thenReturn("users.csv"); + AtomicReference temporaryFile = new AtomicReference<>(); + Mockito.doAnswer(invocation -> { + File file = invocation.getArgument(0); + temporaryFile.set(file); + throw new IOException("disk unavailable"); + }).when(upload).transferTo(Mockito.any(File.class)); + + assertInternalException(() -> + new UserService().multipartFileToFile(upload)); + + Assert.assertNotNull(temporaryFile.get()); + Assert.assertFalse(temporaryFile.get().exists()); + } + + @Test + public void testNullOriginalFilenameUsesSafeTemporaryName() + throws Exception { + MultipartFile upload = Mockito.mock(MultipartFile.class); + Mockito.when(upload.getOriginalFilename()).thenReturn(null); + + File file = new UserService().multipartFileToFile(upload); + + Assert.assertTrue(file.exists()); + Assert.assertTrue(file.delete()); + } + + @Test + public void testExtensionlessFilenameUsesSafeTemporaryName() + throws Exception { + MultipartFile upload = Mockito.mock(MultipartFile.class); + Mockito.when(upload.getOriginalFilename()).thenReturn("users"); + + File file = new UserService().multipartFileToFile(upload); + + Assert.assertTrue(file.exists()); + Assert.assertTrue(file.delete()); + } + + @Test(expected = InternalException.class) + public void testNullCsvFileHasActionableFailure() { + new UserService().readCsvByCsvReader(null); + } + + private static void assertInternalException(ThrowingRunnable runnable) + throws Exception { + try { + runnable.run(); + } catch (InternalException ignored) { + return; + } + Assert.fail("Expected InternalException"); + } + + private interface ThrowingRunnable { + + void run() throws Exception; + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py b/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py index 317606acf..c84385383 100644 --- a/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py +++ b/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py @@ -16,13 +16,14 @@ # under the License. import json -import requests import sys + +import requests from assertpy import assert_that from behave import * -from imp import reload reload(sys) +sys.setdefaultencoding('utf8') use_step_matcher("re") diff --git a/hugegraph-hubble/hubble-be/src/test/resources/application.properties b/hugegraph-hubble/hubble-be/src/test/resources/application.properties index 65eb287d2..627585c3a 100644 --- a/hugegraph-hubble/hubble-be/src/test/resources/application.properties +++ b/hugegraph-hubble/hubble-be/src/test/resources/application.properties @@ -1,4 +1,5 @@ # +# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with this # work for additional information regarding copyright ownership. The ASF @@ -19,7 +20,7 @@ server.servlet.context-path=/api/v1.1 server.port=8088 spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.url=jdbc:h2:mem:db spring.datasource.username=sa spring.datasource.password= spring.datasource.schema=classpath:database/schema.sql @@ -36,6 +37,7 @@ spring.datasource.hikari.connection-test-query=SELECT 1 spring.messages.encoding=UTF-8 spring.messages.basename=i18n/messages +spring.messages.fallback-to-system-locale=false mybatis.type-aliases-package=org.apache.hugegraph.entity mybatis-plus.type-enums-package=org.apache.hugegraph.entity.enums @@ -44,3 +46,9 @@ mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.use-generated-keys=true mybatis.configuration.default-executor-type=reuse mybatis.configuration.default-statement-timeout=600 + +logging.level.org.springframework=WARN +logging.level.org.apache.hugegraph.mapper=DEBUG +logging.level.org.apache.hugegraph.service=INFO +logging.file=logs/hugegraph-hubble.log +logging.file.max-size=10MB diff --git a/hugegraph-hubble/hubble-be/src/test/resources/database/data.sql b/hugegraph-hubble/hubble-be/src/test/resources/database/data.sql index d68b739a6..a590bcb84 100644 --- a/hugegraph-hubble/hubble-be/src/test/resources/database/data.sql +++ b/hugegraph-hubble/hubble-be/src/test/resources/database/data.sql @@ -1,5 +1,6 @@ SELECT 1; /* + * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with this * work for additional information regarding copyright ownership. The ASF diff --git a/hugegraph-hubble/hubble-be/src/test/resources/database/schema.sql b/hugegraph-hubble/hubble-be/src/test/resources/database/schema.sql index c4c646f51..e96fd7d00 100644 --- a/hugegraph-hubble/hubble-be/src/test/resources/database/schema.sql +++ b/hugegraph-hubble/hubble-be/src/test/resources/database/schema.sql @@ -1,18 +1,19 @@ /* + * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + * 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. */ CREATE TABLE IF NOT EXISTS `user_info` ( @@ -29,11 +30,8 @@ CREATE TABLE IF NOT EXISTS `graph_connection` ( `graph` VARCHAR(48) NOT NULL, `host` VARCHAR(48) NOT NULL DEFAULT 'localhost', `port` INT NOT NULL DEFAULT '8080', - `timeout` INT NOT NULL, `username` VARCHAR(48), `password` VARCHAR(48), - `enabled` BOOLEAN NOT NULL DEFAULT true, - `disable_reason` VARCHAR(65535) NOT NULL DEFAULT '', `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`), UNIQUE (`name`), @@ -42,95 +40,20 @@ CREATE TABLE IF NOT EXISTS `graph_connection` ( CREATE TABLE IF NOT EXISTS `execute_history` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, - `async_id` LONG NOT NULL DEFAULT 0, `execute_type` TINYINT NOT NULL, `content` VARCHAR(65535) NOT NULL, `execute_status` TINYINT NOT NULL, - `async_status` TINYINT NOT NULL DEFAULT 0, + `failure_reason` VARCHAR(64) DEFAULT NULL, `duration` LONG NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`) ); -CREATE INDEX IF NOT EXISTS `execute_history_conn_id` ON `execute_history`(`conn_id`); CREATE TABLE IF NOT EXISTS `gremlin_collection` ( `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, `name` VARCHAR(48) NOT NULL, `content` VARCHAR(65535) NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`), - UNIQUE (`conn_id`, `name`) -); -CREATE INDEX IF NOT EXISTS `gremlin_collection_conn_id` ON `gremlin_collection`(`conn_id`); - -CREATE TABLE IF NOT EXISTS `file_mapping` ( - `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, - `job_id` INT NOT NULL DEFAULT 0, - `name` VARCHAR(128) NOT NULL, - `path` VARCHAR(256) NOT NULL, - `total_lines` LONG NOT NULL, - `total_size` LONG NOT NULL, - `file_status` TINYINT NOT NULL DEFAULT 0, - `file_setting` VARCHAR(65535) NOT NULL, - `vertex_mappings` VARCHAR(65535) NOT NULL, - `edge_mappings` VARCHAR(65535) NOT NULL, - `load_parameter` VARCHAR(65535) NOT NULL, - `create_time` DATETIME(6) NOT NULL, - `update_time` DATETIME(6) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE (`conn_id`, `job_id`, `name`) -); -CREATE INDEX IF NOT EXISTS `file_mapping_conn_id` ON `file_mapping`(`conn_id`); - -CREATE TABLE IF NOT EXISTS `load_task` ( - `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL, - `job_id` INT NOT NULL DEFAULT 0, - `file_id` INT NOT NULL, - `file_name` VARCHAR(128) NOT NULL, - `options` VARCHAR(65535) NOT NULL, - `vertices` VARCHAR(512) NOT NULL, - `edges` VARCHAR(512) NOT NULL, - `file_total_lines` LONG NOT NULL, - `load_status` TINYINT NOT NULL, - `file_read_lines` LONG NOT NULL, - `last_duration` LONG NOT NULL, - `curr_duration` LONG NOT NULL, - `create_time` DATETIME(6) NOT NULL, - PRIMARY KEY (`id`) + UNIQUE (`name`) ); - -CREATE TABLE IF NOT EXISTS `job_manager` ( - `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL DEFAULT 0, - `job_name` VARCHAR(100) NOT NULL DEFAULT '', - `job_remarks` VARCHAR(200) NOT NULL DEFAULT '', - `job_size` LONG NOT NULL DEFAULT 0, - `job_status` TINYINT NOT NULL DEFAULT 0, - `job_duration` LONG NOT NULL DEFAULT 0, - `update_time` DATETIME(6) NOT NULL, - `create_time` DATETIME(6) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE (`job_name`, `conn_id`) -); - -CREATE TABLE IF NOT EXISTS `async_task` ( - `id` INT NOT NULL AUTO_INCREMENT, - `conn_id` INT NOT NULL DEFAULT 0, - `task_id` INT NOT NULL DEFAULT 0, - `task_name` VARCHAR(100) NOT NULL DEFAULT '', - `task_reason` VARCHAR(200) NOT NULL DEFAULT '', - `task_type` TINYINT NOT NULL DEFAULT 0, - `algorithm_name` VARCHAR(48) NOT NULL DEFAULT '', - `task_content` VARCHAR(65535) NOT NULL DEFAULT '', - `task_status` TINYINT NOT NULL DEFAULT 0, - `task_duration` LONG NOT NULL DEFAULT 0, - `create_time` DATETIME(6) NOT NULL, - PRIMARY KEY (`id`) -); - -CREATE INDEX IF NOT EXISTS `load_task_conn_id` ON `load_task`(`conn_id`); -CREATE INDEX IF NOT EXISTS `load_task_file_id` ON `load_task`(`file_id`); diff --git a/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/excute_langchain.py b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/excute_langchain.py new file mode 100644 index 000000000..930931701 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/excute_langchain.py @@ -0,0 +1,18 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. + +echo "g.V()" diff --git a/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/fail.py b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/fail.py new file mode 100644 index 000000000..89d5fd152 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/fail.py @@ -0,0 +1,19 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. + +echo "boom" 1>&2 +exit 2 diff --git a/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/other.py b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/other.py new file mode 100644 index 000000000..930931701 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/other.py @@ -0,0 +1,18 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. + +echo "g.V()" diff --git a/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/secret_no_gremlin.py b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/secret_no_gremlin.py new file mode 100644 index 000000000..16565beac --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/secret_no_gremlin.py @@ -0,0 +1,18 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. + +echo "model failed with open-secret" diff --git a/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/sleep.py b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/sleep.py new file mode 100644 index 000000000..9e01cccad --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/resources/langchaincode/sleep.py @@ -0,0 +1,19 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. + +sleep 5 +echo "g.V()" diff --git a/hugegraph-hubble/hubble-be/src/test/resources/log4j2.xml b/hugegraph-hubble/hubble-be/src/test/resources/log4j2.xml deleted file mode 100644 index 3410533ba..000000000 --- a/hugegraph-hubble/hubble-be/src/test/resources/log4j2.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - UTF-8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/hugegraph-hubble/hubble-dist/assembly/descriptor/assembly.xml b/hugegraph-hubble/hubble-dist/assembly/descriptor/assembly.xml index 65ac84d28..addadfe3e 100644 --- a/hugegraph-hubble/hubble-dist/assembly/descriptor/assembly.xml +++ b/hugegraph-hubble/hubble-dist/assembly/descriptor/assembly.xml @@ -39,8 +39,15 @@ / README* - LICENSE* - NOTICE* + + + + ${release.docs.dir} + / + + LICENSE + NOTICE + licenses/** diff --git a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh index cecdb2435..8699689f1 100644 --- a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh +++ b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh @@ -46,12 +46,32 @@ JAVA_OPTS="-Xms512m -Dfile.encoding=UTF-8" JAVA_DEBUG_OPTS="" FOREGROUND="false" -while getopts "f:d" arg; do - case ${arg} in - f) FOREGROUND="$OPTARG" ;; - d) JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n" ;; - ?) echo "USAGE: $0 [-f true|false] [-d] " && exit 1 ;; +while [[ $# -gt 0 ]]; do + case "$1" in + -f|--foreground) + FOREGROUND="true" + if [[ $# -gt 1 && ( "$2" == "true" || "$2" == "false" ) ]]; then + FOREGROUND="$2" + shift + fi + ;; + "-f true"|"--foreground true") + FOREGROUND="true" + ;; + "-f false"|"--foreground false") + FOREGROUND="false" + ;; + -d|--debug) + JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent" + JAVA_DEBUG_OPTS="${JAVA_DEBUG_OPTS} -Xrunjdwp:transport=dt_socket,address=8787" + JAVA_DEBUG_OPTS="${JAVA_DEBUG_OPTS},server=y,suspend=n" + ;; + *) + echo "USAGE: $0 [-f [true|false]] [-d] " + exit 1 + ;; esac + shift done if [[ -f ${PID_FILE} ]] ; then @@ -70,12 +90,12 @@ LOG=${LOG_PATH}/hugegraph-hubble.log if [[ $FOREGROUND == "false" ]]; then echo "Starting Hubble in daemon mode..." - nice -n 0 java -server ${JAVA_OPTS} ${JAVA_DEBUG_OPTS} -Dhubble.home.path="${HOME_PATH}" \ + nohup nice -n 0 java -server ${JAVA_OPTS} ${JAVA_DEBUG_OPTS} -Dhubble.home.path="${HOME_PATH}" \ -cp ${class_path} ${MAIN_CLASS} ${ARGS} > ${LOG} 2>&1 < /dev/null & else echo "Starting Hubble in foreground mode..." - nice -n 0 java -server ${JAVA_OPTS} ${JAVA_DEBUG_OPTS} -Dhubble.home.path="${HOME_PATH}" \ - -cp ${class_path} ${MAIN_CLASS} ${ARGS} > ${LOG} 2>&1 < /dev/null + exec nice -n 0 java -server ${JAVA_OPTS} ${JAVA_DEBUG_OPTS} -Dhubble.home.path="${HOME_PATH}" \ + -cp ${class_path} ${MAIN_CLASS} ${ARGS} fi PID=$! diff --git a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties index 8d90328c3..1faf8a13b 100644 --- a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties +++ b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties @@ -22,3 +22,28 @@ gremlin.suffix_limit=250 gremlin.vertex_degree_limit=100 gremlin.edges_total_limit=500 gremlin.batch_query_ids=100 + +cluster=hg +idc=bddwd + +# ===== Deployment Mode ===== +# Set to false for standalone RocksDB mode (no PD dependency) +pd.enabled=false +# Direct server URL, only used when pd.enabled=false +server.direct_url=http://127.0.0.1:8080 + +# pd +pd.peers=127.0.0.1:8686 +pd.server=127.0.0.1:8620 + +# dashboard +dashboard.address=127.0.0.1:8092 +# BOTH, NODE_PORT, DDS +route.type=NODE_PORT + +# Set monitor url +monitor.url= +prometheus.url=http://127.0.0.1:8090 + +# ES +es.urls= diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/check-hubble-dist.sh b/hugegraph-hubble/hubble-dist/assembly/travis/check-hubble-dist.sh new file mode 100755 index 000000000..dfae36887 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/check-hubble-dist.sh @@ -0,0 +1,267 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +set -euo pipefail + +usage() { + echo "Usage: $0 " \ + "[--json-output ] [--require-sidecars]" >&2 +} + +if [[ $# -lt 1 ]]; then + usage + exit 1 +fi + +tarball=$1 +shift +json_output="" +require_sidecars=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --json-output) + if [[ $# -lt 2 ]]; then + usage + exit 1 + fi + json_output=$2 + shift 2 + ;; + --require-sidecars) + require_sidecars=true + shift + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ ! -f "${tarball}" ]]; then + echo "Hubble tarball not found: ${tarball}" >&2 + exit 1 +fi + +tmp_list=$(mktemp) +tmp_dir=$(mktemp -d) +native_list=$(mktemp) +trap 'rm -f "${tmp_list}" "${native_list}"; rm -rf "${tmp_dir}"' EXIT + +tar -tzf "${tarball}" > "${tmp_list}" + +if grep -qE '(^/|(^|/)\.\.(/|$))' "${tmp_list}"; then + echo "Unsafe tarball path found; absolute paths and '..' are not allowed" >&2 + grep -E '(^/|(^|/)\.\.(/|$))' "${tmp_list}" >&2 + exit 1 +fi + +if tar -tvzf "${tarball}" | grep -qE '^[lh]'; then + echo "Tarball must not contain symbolic or hard links" >&2 + tar -tvzf "${tarball}" | grep -E '^[lh]' >&2 + exit 1 +fi + +root_count=$(awk -F/ 'NF > 0 && $1 != "" {print $1}' "${tmp_list}" | sort -u | wc -l | tr -d ' ') +root=$(awk -F/ 'NF > 0 && $1 != "" {print $1; exit}' "${tmp_list}") + +if [[ -z "${root}" || "${root}" == "." ]]; then + echo "Unable to resolve tarball root directory" >&2 + exit 1 +fi + +if [[ "${root_count}" -ne 1 ]]; then + echo "Tarball must contain exactly one top-level root directory" >&2 + awk -F/ 'NF > 0 && $1 != "" {print $1}' "${tmp_list}" | sort -u >&2 + exit 1 +fi + +tar -xzf "${tarball}" -C "${tmp_dir}" + +required_paths=( + "${root}/bin/" + "${root}/bin/start-hubble.sh" + "${root}/bin/stop-hubble.sh" + "${root}/bin/common_functions" + "${root}/conf/" + "${root}/conf/hugegraph-hubble.properties" + "${root}/lib/" + "${root}/ui/" + "${root}/ui/index.html" + "${root}/README.md" + "${root}/LICENSE" + "${root}/NOTICE" + "${root}/licenses/" +) + +for path in "${required_paths[@]}"; do + if ! grep -qxF "${path}" "${tmp_list}"; then + echo "Missing required distribution path: ${path}" >&2 + exit 1 + fi +done + +for legal_file in LICENSE NOTICE README.md; do + if [[ -z "$(tr -d '[:space:]' < "${tmp_dir}/${root}/${legal_file}")" ]]; then + echo "Packaged ${legal_file} must not be empty" >&2 + exit 1 + fi +done + +if ! grep -qE "^${root}/licenses/fe-licenses/" "${tmp_list}"; then + echo "Missing frontend license directory: ${root}/licenses/fe-licenses/" >&2 + exit 1 +fi + +for residue in logs upload-files; do + if grep -qE "^${root}/${residue}(/|$)" "${tmp_list}"; then + echo "Runtime residue must not be packaged: ${residue}" >&2 + exit 1 + fi +done + +blocked_patterns=( + "node_modules" +) + +for pattern in "${blocked_patterns[@]}"; do + if grep -qE "(^|/)${pattern}(/|$)" "${tmp_list}"; then + echo "Blocked runtime/development artifact found: ${pattern}" >&2 + grep -E "(^|/)${pattern}(/|$)" "${tmp_list}" >&2 + exit 1 + fi +done + +blocked_file_pattern="(^|/)(pid|[^/]+\\.(pid|lock|db|log))$" +if grep -qE "${blocked_file_pattern}" "${tmp_list}"; then + echo "Blocked runtime/development file found" >&2 + grep -E "${blocked_file_pattern}" "${tmp_list}" >&2 + exit 1 +fi + +if grep -qE "^${root}/ui/.*\\.map$" "${tmp_list}"; then + echo "Frontend source maps must not be packaged" >&2 + grep -E "^${root}/ui/.*\\.map$" "${tmp_list}" >&2 + exit 1 +fi + +unknown_binary_pattern="^${root}/.*\\.(so|dylib|dll|exe|bin|tar|tgz|gz|zip)$" +if grep -qE "${unknown_binary_pattern}" "${tmp_list}"; then + echo "Unknown outer binary/archive files found in distribution" >&2 + grep -E "${unknown_binary_pattern}" "${tmp_list}" >&2 + exit 1 +fi + +jar_count=$(grep -cE "^${root}/lib/[^/]+\\.jar$" "${tmp_list}" || true) +if [[ "${jar_count}" -eq 0 ]]; then + echo "No dependency jars found under ${root}/lib" >&2 + exit 1 +fi + +category_x_patterns=( + "^${root}/lib/truelicense-[^/]+\\.jar$" +) + +category_x_hits="" +for pattern in "${category_x_patterns[@]}"; do + matches=$(grep -E "${pattern}" "${tmp_list}" || true) + if [[ -n "${matches}" ]]; then + category_x_hits+="${matches}"$'\n' + fi +done + +if [[ -n "${category_x_hits}" ]]; then + echo "ASF Category-X dependency must not be packaged:" >&2 + printf '%s' "${category_x_hits}" >&2 + exit 1 +fi + +license_count=$(grep -cE "^${root}/licenses/[^/]+$" "${tmp_list}" || true) +fe_license_count=$(grep -cE "^${root}/licenses/fe-licenses/[^/]+$" \ + "${tmp_list}" || true) + +if [[ "${license_count}" -eq 0 ]]; then + echo "No dependency license files found under ${root}/licenses" >&2 + exit 1 +fi + +if [[ "${fe_license_count}" -eq 0 ]]; then + echo "No frontend license files found under ${root}/licenses/fe-licenses" >&2 + exit 1 +fi + +while IFS= read -r jar_path; do + local_jar="${tmp_dir}/${jar_path}" + if jar tf "${local_jar}" | grep -qE "\\.(so|dylib|dll|jnilib)$"; then + echo "${jar_path}" >> "${native_list}" + fi +done < <(grep -E "^${root}/lib/[^/]+\\.jar$" "${tmp_list}") + +native_jar_count=$(wc -l < "${native_list}" | tr -d ' ') +checksum_status="not_checked" +signature_status="not_checked" + +if [[ -f "${tarball}.sha512" ]]; then + expected_sha512=$(awk '{print $1}' "${tarball}.sha512") + actual_sha512=$(shasum -a 512 "${tarball}" | awk '{print $1}') + if [[ "${expected_sha512}" != "${actual_sha512}" ]]; then + echo "SHA-512 checksum mismatch for ${tarball}" >&2 + exit 1 + fi + checksum_status="passed" +elif [[ "${require_sidecars}" == "true" ]]; then + echo "Missing required SHA-512 sidecar: ${tarball}.sha512" >&2 + exit 1 +fi + +if [[ -f "${tarball}.asc" ]]; then + if ! command -v gpg >/dev/null 2>&1; then + echo "gpg is required to verify signature sidecar: ${tarball}.asc" >&2 + exit 1 + fi + gpg --verify "${tarball}.asc" "${tarball}" >/dev/null 2>&1 + signature_status="passed" +elif [[ "${require_sidecars}" == "true" ]]; then + echo "Missing required signature sidecar: ${tarball}.asc" >&2 + exit 1 +fi + +if [[ -n "${json_output}" ]]; then + mkdir -p "$(dirname "${json_output}")" + { + echo "{" + echo " \"tarball\": \"${tarball}\"," + echo " \"root\": \"${root}\"," + echo " \"jar_count\": ${jar_count}," + echo " \"license_count\": ${license_count}," + echo " \"fe_license_count\": ${fe_license_count}," + echo " \"category_x_status\": \"passed\"," + echo " \"checksum_status\": \"${checksum_status}\"," + echo " \"signature_status\": \"${signature_status}\"," + echo " \"native_jar_count\": ${native_jar_count}," + echo " \"native_jars\": [" + sed 's#^# "#; s#$#"#; $!s#$#,#' "${native_list}" + echo " ]" + echo "}" + } > "${json_output}" +fi + +echo "Hubble distribution check passed: ${tarball}" +printf 'JARs: %s, license files: %s, FE license files: %s, native-bearing JARs: %s\n' \ + "${jar_count}" "${license_count}" "${fe_license_count}" "${native_jar_count}" diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh b/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh index 8cce7d092..df4abd4e2 100755 --- a/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh +++ b/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh @@ -18,18 +18,39 @@ export LANG=zh_CN.UTF-8 set -ev -if [[ $# -ne 1 ]]; then - echo "Must input an existing commit id of hugegraph server" && exit 1 +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 [fetch-ref]" >&2 + exit 1 fi COMMIT_ID=$1 +COMMIT_REF=${2:-} HUGEGRAPH_GIT_URL="https://github.com/apache/hugegraph.git" GIT_DIR=hugegraph +CACHE_DIR="${HOME}/hugegraph-cache-${COMMIT_ID}" + +mkdir -p "${CACHE_DIR}" +CACHED_TARBALL=$(find "${CACHE_DIR}" -maxdepth 1 \ + -name "apache-hugegraph-*.tar.gz" -print -quit) + +if [[ -f "${CACHED_TARBALL}" ]]; then + echo "Found HugeGraph server tarball cached for commit ${COMMIT_ID}." + cp "${CACHED_TARBALL}" ./ + exit 0 +fi # download code and compile git clone --depth 150 $HUGEGRAPH_GIT_URL $GIT_DIR cd "${GIT_DIR}" +if [[ -n "${COMMIT_REF}" ]]; then + git fetch --depth 1 origin "${COMMIT_REF}" +fi git checkout "${COMMIT_ID}" +ACTUAL_COMMIT_ID=$(git rev-parse HEAD) +if [[ "${ACTUAL_COMMIT_ID}" != "${COMMIT_ID}" ]]; then + echo "HugeGraph checkout mismatch: expected ${COMMIT_ID}, got ${ACTUAL_COMMIT_ID}" >&2 + exit 1 +fi mvn package -DskipTests -Dmaven.javadoc.skip=true -ntp cd hugegraph-server @@ -37,3 +58,4 @@ TAR=$(echo apache-hugegraph-*.tar.gz) cp apache-hugegraph-*.tar.gz ../../ cd ../../ rm -rf "${GIT_DIR}" +cp apache-hugegraph-*.tar.gz "${CACHE_DIR}"/ diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/install-hugegraph.sh b/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/install-hugegraph.sh index cb992cbe1..dbc7f8e77 100755 --- a/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/install-hugegraph.sh +++ b/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/install-hugegraph.sh @@ -35,5 +35,5 @@ cp "${SERVER_CONFIG_DIR}"/graphs/hugegraph2.properties "${SERVER_DIR}"/conf/grap cd "${SERVER_DIR}" && pwd -bin/init-store.sh || exit 1 +echo -e "pa" | bin/init-store.sh || exit 1 bin/start-hugegraph.sh || exit 1 diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/rest-server.properties b/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/rest-server.properties index e95546069..449a4575c 100644 --- a/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/rest-server.properties +++ b/hugegraph-hubble/hubble-dist/assembly/travis/hugegraph-server1/rest-server.properties @@ -23,6 +23,6 @@ gremlinserver.url=http://127.0.0.1:8182 graphs=./conf/graphs # authentication -#auth.require_authentication= -#auth.admin_token= -#auth.user_tokens=[] +auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator +auth.admin_token=162f7848-0b6d-4faf-b557-3a0797869c55 +auth.user_tokens=[hugegraph1:9fd95c9c-711b-415b-b85f-d4df46ba5c31,hugegraph2:9fd95c9c-711b-415b-b85f-d4df46ba5c32] diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/install-hugegraph.sh b/hugegraph-hubble/hubble-dist/assembly/travis/install-hugegraph.sh index 920968c00..f352283c0 100755 --- a/hugegraph-hubble/hubble-dist/assembly/travis/install-hugegraph.sh +++ b/hugegraph-hubble/hubble-dist/assembly/travis/install-hugegraph.sh @@ -18,8 +18,14 @@ export LANG=zh_CN.UTF-8 set -ev +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 [fetch-ref]" >&2 + exit 1 +fi + COMMIT_ID=$1 +COMMIT_REF=${2:-} -"$TRAVIS_DIR"/download-hugegraph.sh "$COMMIT_ID" +"$TRAVIS_DIR"/download-hugegraph.sh "$COMMIT_ID" "$COMMIT_REF" "$TRAVIS_DIR"/hugegraph-server1/install-hugegraph.sh "$TRAVIS_DIR"/hugegraph-server2/install-hugegraph.sh diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/run_algorithm_api_inventory.py b/hugegraph-hubble/hubble-dist/assembly/travis/run_algorithm_api_inventory.py new file mode 100755 index 000000000..4aec356cf --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_algorithm_api_inventory.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +import argparse +import json +import re +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[4] +FE_ROOT = REPO_ROOT / "hugegraph-hubble" / "hubble-fe" / "src" +BE_ROOT = REPO_ROOT / "hugegraph-hubble" / "hubble-be" / "src" / "main" / "java" +COMPATIBILITY_ALIASES = { + "allshortpath": "allshortestpaths", + "shortpath": "shortestPath", +} + + +def parse_object_string_entries(body): + pattern = re.compile( + r"^\s*(?:(?P['\"])(?P\w+)(?P=key_quote)|" + r"(?P\w+))\s*:\s*(?P['\"])" + r"(?P(?:\\.|(?!(?P=value_quote)).)*)(?P=value_quote)", + re.MULTILINE + ) + entries = [] + for match in pattern.finditer(body): + entries.append(( + match.group("quoted_key") or match.group("bare_key"), + unescape_js_string(match.group("value")) + )) + return entries + + +def parse_algorithm_url_entries(body): + pattern = re.compile( + r"\[\s*ALGORITHM_NAME\.(?P\w+)\s*\]\s*:\s*" + r"(?P['\"])(?P(?:\\.|(?!(?P=quote)).)*)(?P=quote)" + ) + entries = [] + for match in pattern.finditer(body): + entries.append(( + match.group("symbol"), + unescape_js_string(match.group("url")) + )) + return entries + + +def unescape_js_string(value): + return value.replace("\\'", "'").replace('\\"', '"').replace("\\\\", "\\") + + +def parse_algorithm_names(): + source = FE_ROOT / "utils" / "constants.js" + text = source.read_text(encoding="utf-8") + match = re.search(r"export const ALGORITHM_NAME = \{(?P.*?)\};", text, + re.DOTALL) + if not match: + raise RuntimeError("Unable to find ALGORITHM_NAME in constants.js") + return dict(parse_object_string_entries(match.group("body"))) + + +def parse_frontend_algorithm_urls(algorithm_names): + source = FE_ROOT / "utils" / "constants.js" + text = source.read_text(encoding="utf-8") + match = re.search(r"export const Algorithm_Url = \{(?P.*?)\};", text, + re.DOTALL) + if not match: + raise RuntimeError("Unable to find Algorithm_Url in constants.js") + urls = [] + seen = set() + for symbol, url in parse_algorithm_url_entries(match.group("body")): + key = (symbol, url) + if key in seen: + continue + seen.add(key) + urls.append({ + "symbol": symbol, + "ui_name": algorithm_names.get(symbol, symbol), + "frontend_url": url, + "source": str(source.relative_to(REPO_ROOT)) + }) + return urls + + +def parse_backend_algorithm_endpoints(): + controller = (BE_ROOT / "org" / "apache" / "hugegraph" / "controller" / + "algorithm" / "OltpAlgoController.java") + text = controller.read_text(encoding="utf-8") + endpoints = [] + for annotation in re.findall(r"@PostMapping\(([^)]*)\)", text): + endpoints.extend(re.findall(r"\"([^\"]+)\"", annotation)) + return sorted(set(endpoints)) + + +def controller_has_mapping(controller_name, pattern): + controller = (BE_ROOT / "org" / "apache" / "hugegraph" / "controller" / + controller_name) + text = controller.read_text(encoding="utf-8") + return re.search(pattern, text, re.DOTALL) is not None + + +def write_report(path, inventory, boundary): + aliases = inventory_aliases(boundary) + backend_only = inventory_backend_only(boundary) + lines = [ + "# Hubble Algorithm API Inventory", + "", + "Generated from source code. This classifies FE OLTP algorithm slugs " + "against Hubble BE OLTP controller routes. OLAP, Vermeer, and Cypher " + "are boundary-route checks; this is not live API proof.", + "", + "## Summary", + "", + "| Metric | Count |", + "|-|-|", + f"| FE OLTP algorithm slugs | {len(inventory)} |", + f"| BE compatibility aliases | {len(aliases)} |", + f"| Non-alias BE-only endpoints | {len(backend_only)} |", + "", + "## FE OLTP Slug Inventory", + "", + "| UI algorithm | Frontend slug | Hubble BE endpoint | Status |", + "|-|-|-|-|", + ] + for item in inventory: + endpoint = item["backend_endpoint"] or "" + lines.append("| {ui_name} | {frontend_url} | {endpoint} | {status} |".format( + ui_name=item["ui_name"], + frontend_url=item["frontend_url"] or "", + endpoint=endpoint, + status=item["status"] + )) + lines.extend([ + "", + "## Boundary Routes", + "", + "| Area | Hubble route present | Verification scope |", + "|-|-|-|", + ]) + for item in boundary: + lines.append("| {area} | {present} | {scope} |".format( + area=item["area"], + present="yes" if item["route_present"] else "no", + scope=item["verification_scope"] + )) + lines.extend([ + "", + "## Backend Compatibility Aliases", + "", + "| Backend endpoint | Canonical endpoint | Status |", + "|-|-|-|", + ]) + for item in sorted(inventory_aliases(boundary), key=lambda alias: alias["endpoint"]): + lines.append("| {endpoint} | {canonical} | {status} |".format( + endpoint=item["endpoint"], + canonical=item["canonical_endpoint"], + status=item["status"] + )) + lines.extend([ + "", + "## Non-Alias Backend-Only Endpoints", + "", + "| Backend endpoint | Status |", + "|-|-|", + ]) + if backend_only: + for endpoint in backend_only: + lines.append(f"| {endpoint} | backend-only-without-frontend-slug |") + else: + lines.append("| | none |") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def inventory_aliases(boundary): + for item in boundary: + if item.get("area") == "OLTP algorithms": + return item.get("compatibility_aliases", []) + return [] + + +def inventory_backend_only(boundary): + for item in boundary: + if item.get("area") == "OLTP algorithms": + return item.get("backend_only_endpoints", []) + return [] + + +def main(): + parser = argparse.ArgumentParser(description="Inventory Hubble algorithm API boundaries") + parser.add_argument("--json-output", type=Path, + help="Optional path for machine-readable inventory") + parser.add_argument("--markdown-output", type=Path, + help="Optional path for markdown inventory") + parser.add_argument("--self-test", action="store_true", + help="Run parser self-tests and exit") + args = parser.parse_args() + if args.self_test: + run_self_tests() + return + + algorithm_names = parse_algorithm_names() + frontend_urls = parse_frontend_algorithm_urls(algorithm_names) + backend_endpoints = parse_backend_algorithm_endpoints() + backend_endpoint_set = set(backend_endpoints) + inventory = [] + + for frontend in frontend_urls: + frontend_url = frontend.get("frontend_url") + backend_endpoint = None + status = "frontend-listed-without-hubble-be-route" + if frontend_url in backend_endpoint_set: + backend_endpoint = frontend_url + status = "supported-by-hubble-be" + inventory.append({ + "symbol": frontend["symbol"], + "ui_name": frontend["ui_name"], + "frontend_url": frontend_url, + "frontend_source": frontend.get("source"), + "backend_endpoint": backend_endpoint, + "status": status + }) + + frontend_url_set = {item["frontend_url"] for item in frontend_urls} + compatibility_aliases = [ + { + "endpoint": endpoint, + "canonical_endpoint": COMPATIBILITY_ALIASES[endpoint], + "status": "backend-only-compatibility-alias" + } + for endpoint in backend_endpoints + if endpoint in COMPATIBILITY_ALIASES + ] + backend_only = [ + endpoint for endpoint in backend_endpoints + if endpoint not in frontend_url_set and endpoint not in COMPATIBILITY_ALIASES + ] + boundary = [ + { + "area": "OLTP algorithms", + "route_present": len(backend_endpoints) > 0, + "compatibility_aliases": compatibility_aliases, + "backend_only_endpoints": backend_only, + "verification_scope": ("source inventory for all routes; " + "live smoke covers shortestPath") + }, + { + "area": "OLAP algorithms", + "route_present": controller_has_mapping( + "algorithm/OlapAlgoController.java", + r"algorithms/olap" + ), + "verification_scope": ("source route inventory only; live execution " + "depends on computer backend configuration") + }, + { + "area": "Vermeer algorithms", + "route_present": controller_has_mapping( + "algorithm/VermeerAlgoController.java", + r"algorithms/vermeer" + ), + "verification_scope": ("source route inventory only; live execution " + "depends on Vermeer graph loading") + }, + { + "area": "Cypher", + "route_present": controller_has_mapping( + "query/CypherController.java", + r"/\{graph\}/cypher" + ), + "verification_scope": "source route inventory plus optional live smoke" + }, + ] + + result = { + "backend_algorithm_endpoints": backend_endpoints, + "backend_compatibility_aliases": compatibility_aliases, + "backend_only_endpoints": backend_only, + "boundary_routes": boundary, + "inventory": inventory, + "summary": { + "frontend_algorithm_count": len(frontend_urls), + "backend_algorithm_endpoint_count": len(backend_endpoints), + "supported_by_hubble_be_count": sum( + 1 for item in inventory if item["status"] == "supported-by-hubble-be" + ), + "frontend_only_count": sum( + 1 for item in inventory + if item["status"] == "frontend-listed-without-hubble-be-route" + ), + "backend_compatibility_alias_count": len(compatibility_aliases), + "backend_only_count": len(backend_only) + } + } + + if args.json_output: + args.json_output.parent.mkdir(parents=True, exist_ok=True) + args.json_output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", + encoding="utf-8") + if args.markdown_output: + args.markdown_output.parent.mkdir(parents=True, exist_ok=True) + write_report(args.markdown_output, inventory, boundary) + + print(json.dumps(result["summary"], sort_keys=True)) + if result["summary"]["frontend_algorithm_count"] == 0: + raise SystemExit("No FE algorithm URLs found") + if result["summary"]["backend_algorithm_endpoint_count"] == 0: + raise SystemExit("No Hubble BE algorithm endpoints found") + if result["summary"]["frontend_only_count"] > 0: + raise SystemExit("Some FE algorithm slugs have no Hubble BE route") + if result["summary"]["backend_only_count"] > 0: + raise SystemExit("Some non-alias Hubble BE endpoints have no FE slug") + if not all(item["route_present"] for item in boundary): + raise SystemExit("Some Hubble analysis boundary routes are missing") + + +def run_self_tests(): + names = parse_object_string_entries(""" + PAGE_RANK: 'PageRank', + "K_OUT": "K-out", + 'QUOTE': 'Don\\'t stop', + """) + assert dict(names) == { + "PAGE_RANK": "PageRank", + "K_OUT": "K-out", + "QUOTE": "Don't stop", + } + + urls = parse_frontend_algorithm_urls({"PAGE_RANK": "PageRank", "K_OUT": "K-out"}) + assert urls, "repository Algorithm_Url entries should parse" + + synthetic_url_body = """ + [ALGORITHM_NAME.PAGE_RANK]: "pageRank", + [ALGORITHM_NAME.K_OUT]: 'kout', + """ + assert parse_algorithm_url_entries(synthetic_url_body) == [ + ("PAGE_RANK", "pageRank"), + ("K_OUT", "kout") + ] + assert COMPATIBILITY_ALIASES["shortpath"] == "shortestPath" + print(json.dumps({"status": "passed", "selfTests": 4}, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py b/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py new file mode 100755 index 000000000..0fbf6518e --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py @@ -0,0 +1,888 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +import argparse +import base64 +import gzip +import http.cookiejar +import json +import os +import re +import shutil +import subprocess +import tarfile +import tempfile +import time +import urllib.error +import urllib.parse +import urllib.request +import uuid +from pathlib import Path + + +COOKIE_JAR = http.cookiejar.CookieJar() +OPENER = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(COOKIE_JAR) +) +SERVER_TOKEN = None + + +def request(method, url, body=None, headers=None, timeout=10): + data = None + merged_headers = {"Accept-Encoding": "identity", **(headers or {})} + if body is not None: + if isinstance(body, bytes): + data = body + else: + data = json.dumps(body).encode("utf-8") + merged_headers = {"Content-Type": "application/json", + **merged_headers} + req = urllib.request.Request(url, data=data, headers=merged_headers, + method=method) + with OPENER.open(req, timeout=timeout) as response: + raw_payload = response.read() + if response.headers.get("Content-Encoding") == "gzip": + raw_payload = gzip.decompress(raw_payload) + payload = raw_payload.decode("utf-8") + content_type = response.headers.get("Content-Type", "") + if ("application/json" in content_type or "+json" in content_type) and payload: + return json.loads(payload) + return payload + + +def unwrap(response, name): + if not isinstance(response, dict) or response.get("status") != 200: + raise RuntimeError(f"{name} failed: {response}") + return response.get("data") + + +def server_json(method, server_url, path, body=None, timeout=10): + headers = {} + if SERVER_TOKEN: + headers["Authorization"] = f"Bearer {SERVER_TOKEN}" + return request(method, f"{server_url}{path}", body, headers=headers, + timeout=timeout) + + +def is_hubble_readiness_response(response): + if not isinstance(response, dict) or response.get("status") != 200: + return False + data = response.get("data") + return (isinstance(data, dict) and + isinstance(data.get("name"), str) and bool(data["name"]) and + isinstance(data.get("version"), str) and bool(data["version"])) + + +def wait_for_health(hubble_url, deadline_seconds): + deadline = time.time() + deadline_seconds + last_error = None + while time.time() < deadline: + try: + response = request("GET", f"{hubble_url}/about", timeout=3) + if is_hubble_readiness_response(response): + return response + last_error = RuntimeError(f"Unexpected /about response: {response}") + except Exception as exc: # noqa: BLE001 - smoke tool should report final error + last_error = exc + time.sleep(1) + raise RuntimeError(f"Hubble health did not become UP: {last_error}") + + +def is_healthy(hubble_url): + try: + response = request("GET", f"{hubble_url}/about", timeout=2) + return is_hubble_readiness_response(response) + except Exception: # noqa: BLE001 - preflight only needs a boolean + return False + + +def assert_safe_tar_member(member, work_dir): + target = (work_dir / member.name).resolve() + root = work_dir.resolve() + if target != root and root not in target.parents: + raise RuntimeError(f"Unsafe tar entry outside work dir: {member.name}") + if member.islnk() or member.issym(): + link_target = (target.parent / member.linkname).resolve() + if link_target != root and root not in link_target.parents: + raise RuntimeError(f"Unsafe tar link outside work dir: {member.name}") + + +def extract_tarball(tarball, work_dir): + with tarfile.open(tarball, "r:gz") as archive: + members = archive.getmembers() + for member in members: + assert_safe_tar_member(member, work_dir) + archive.extractall(work_dir, members) + homes = [path for path in work_dir.iterdir() + if path.is_dir() and path.name.startswith("apache-hugegraph-hubble-")] + if not homes: + raise RuntimeError(f"Unable to find Hubble home under {work_dir}") + return homes[0] + + +def configure_hubble_endpoint(hubble_home, hubble_url, bind_host, server_url): + parsed = urllib.parse.urlparse(hubble_url) + host = bind_host or parsed.hostname + port = parsed.port + conf = hubble_home / "conf" / "hugegraph-hubble.properties" + text = conf.read_text(encoding="utf-8") + lines = [] + replaced_host = False + replaced_port = False + replaced_server_address = False + replaced_server_port = False + replaced_pd_enabled = False + replaced_direct_url = False + for line in text.splitlines(): + if host and line.startswith("hubble.host="): + lines.append(f"hubble.host={host}") + replaced_host = True + elif port and line.startswith("hubble.port="): + lines.append(f"hubble.port={port}") + replaced_port = True + elif host and line.startswith("server.address="): + lines.append(f"server.address={host}") + replaced_server_address = True + elif port and line.startswith("server.port="): + lines.append(f"server.port={port}") + replaced_server_port = True + elif line.startswith("pd.enabled="): + lines.append("pd.enabled=false") + replaced_pd_enabled = True + elif line.startswith("server.direct_url="): + lines.append(f"server.direct_url={server_url}") + replaced_direct_url = True + else: + lines.append(line) + if host and not replaced_host: + lines.append(f"hubble.host={host}") + if host and not replaced_server_address: + lines.append(f"server.address={host}") + if port and not replaced_port: + lines.append(f"hubble.port={port}") + if port and not replaced_server_port: + lines.append(f"server.port={port}") + if not replaced_pd_enabled: + lines.append("pd.enabled=false") + if not replaced_direct_url: + lines.append(f"server.direct_url={server_url}") + conf.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def run_hubble_only_checks(hubble_url): + checks = [] + readiness = request("GET", f"{hubble_url}/about") + if not is_hubble_readiness_response(readiness): + raise RuntimeError(f"Unexpected Hubble /about response: {readiness}") + checks.append({"name": "hubble-readiness", "status": "passed", + "detail": readiness}) + root = request("GET", f"{hubble_url}/") + if '
' not in root: + raise RuntimeError("Hubble root did not serve React root") + checks.append({"name": "hubble-ui-root", "status": "passed"}) + for route in ( + "/navigation", + "/graphspace", + "/gremlin", + "/algorithms", + "/asyncTasks", + ): + body = request("GET", f"{hubble_url}{route}") + if '
' not in body: + raise RuntimeError(f"Route did not serve React root: {route}") + checks.append({"name": f"route:{route}", "status": "passed"}) + return checks + + +def run_login_checks(hubble_url, username, password): + checks = [] + api_prefix = f"{hubble_url}/api/v1.3" + user = unwrap(request("POST", f"{api_prefix}/auth/login", { + "user_name": username, + "user_password": password + }), "hubble-login") + user_name = (user.get("user_name") or user.get("name") or + user.get("id") or "") + if str(user_name) != username: + raise RuntimeError(f"Unexpected login user: {user}") + checks.append({"name": "hubble-login", "status": "passed", + "user": username}) + + auth_status = unwrap(request("GET", f"{api_prefix}/auth/status"), + "hubble-auth-status") + if "level" not in auth_status: + raise RuntimeError(f"Unexpected auth status response: {auth_status}") + checks.append({"name": "hubble-auth-status", "status": "passed", + "level": auth_status.get("level")}) + return checks + + +def run_server_login(server_url, username, password): + global SERVER_TOKEN + auth = f"{username}:{password}" + basic = base64.b64encode(auth.encode("utf-8")).decode("ascii") + response = request("POST", f"{server_url}/auth/login", { + "user_name": username, + "user_password": password, + "token_expire": 60 * 60 * 24 * 30 + }, headers={"Authorization": f"Basic {basic}"}) + token = response.get("token") if isinstance(response, dict) else None + if not token: + raise RuntimeError(f"Unexpected Server login response: {response}") + SERVER_TOKEN = token + return [{"name": "server-login", "status": "passed", + "user": username}] + + +def run_server_checks(hubble_url, server_url, graph_space, graph_name): + checks = [] + api_prefix = f"{hubble_url}/api/v1.3" + for name, path in ( + ("graphspace-list", "graphspaces/list"), + ("schema-graphview", + f"graphspaces/{graph_space}/graphs/{graph_name}/schema/graphview"), + ("job-manager-list", + f"graphspaces/{graph_space}/graphs/{graph_name}/job-manager" + "?page_no=1&page_size=10"), + ("async-task-list", + f"graphspaces/{graph_space}/graphs/{graph_name}/async-tasks" + "?page_no=1&page_size=10"), + ): + response = request("GET", f"{api_prefix}/{path}") + if response.get("status") != 200: + raise RuntimeError(f"{name} failed: {response}") + checks.append({"name": name, "status": "passed"}) + versions = request("GET", f"{server_url}/versions") + checks.append({"name": "server-versions", "status": "passed", + "detail_type": type(versions).__name__}) + return checks + + +def get_json_status(method, url, body=None): + try: + response = request(method, url, body) + if isinstance(response, dict): + return response.get("status", 200) + return 200 + except urllib.error.HTTPError as exc: + try: + payload = exc.read().decode("utf-8") + response = json.loads(payload) + return response.get("status", exc.code) + except (UnicodeDecodeError, json.JSONDecodeError): + return exc.code + + +def run_analysis_boundary_checks(hubble_url, graph_space, graph_name): + base = f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/{graph_name}" + checks = [] + + cypher_status = get_json_status( + "GET", + f"{base}/cypher?cypher={urllib.parse.quote('MATCH (n) RETURN n LIMIT 1')}" + ) + if cypher_status not in (200, 400): + raise RuntimeError( + f"analysis-cypher-boundary failed: status {cypher_status}") + checks.append({ + "name": "analysis-cypher-boundary", + "status": "passed" if cypher_status == 200 else "skipped", + "http_or_business_status": cypher_status, + "classification": ("hubble-api-available" + if cypher_status == 200 else + "boundary-or-environment-dependent") + }) + + olap_status = get_json_status("POST", f"{base}/algorithms/olap", { + "algorithm": "pagerank", + "worker": 1, + "params": {} + }) + if olap_status not in (200, 400): + raise RuntimeError( + f"analysis-olap-boundary failed: status {olap_status}") + checks.append({ + "name": "analysis-olap-boundary", + "status": "passed" if olap_status == 200 else "skipped", + "http_or_business_status": olap_status, + "classification": ("hubble-api-available" + if olap_status == 200 else + "boundary-or-environment-dependent") + }) + + return checks + + +def ignore_conflict(call): + try: + return call() + except urllib.error.HTTPError as exc: + if exc.code == 400: + return None + raise + + +def create_schema(hubble_url, server_url, graph_space, graph_name, prefix): + base = (f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/schema") + pk_id = f"{prefix}_id" + pk_name = f"{prefix}_name" + pk_rank = f"{prefix}_rank" + vl_person = f"{prefix}_person" + el_knows = f"{prefix}_knows" + + checks = [] + for name, data_type in ((pk_id, "TEXT"), (pk_name, "TEXT"), + (pk_rank, "INT")): + body = {"name": name, "data_type": data_type, "cardinality": "SINGLE"} + response = ignore_conflict( + lambda body=body: request("POST", f"{base}/propertykeys", body) + ) + if response is not None: + unwrap(response, f"create property key {name}") + checks.append({"name": "hubble-schema-propertykeys", "status": "passed"}) + + vertex_body = { + "name": vl_person, + "id_strategy": "CUSTOMIZE_STRING", + "properties": [ + {"name": pk_name, "nullable": True}, + {"name": pk_rank, "nullable": True} + ], + "primary_keys": [], + "property_indexes": [], + "open_label_index": True, + "style": {"color": "#2B65FF", "icon": "user", + "display_fields": []} + } + response = ignore_conflict( + lambda: request("POST", f"{base}/vertexlabels", vertex_body) + ) + if response is not None: + unwrap(response, f"create vertex label {vl_person}") + checks.append({"name": "hubble-schema-vertexlabel", "status": "passed", + "label": vl_person}) + + edge_body = { + "name": el_knows, + "edgelabel_type": "NORMAL", + "source_label": vl_person, + "target_label": vl_person, + "link_multi_times": False, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": True, + "style": {"color": "#0EB880", "display_fields": []} + } + response = ignore_conflict( + lambda: request("POST", f"{base}/edgelabels", edge_body) + ) + if response is not None: + unwrap(response, f"create edge label {el_knows}") + checks.append({"name": "hubble-schema-edgelabel", "status": "passed", + "label": el_knows}) + + direct = server_json("GET", server_url, + f"/graphs/{graph_name}/schema/vertexlabels/{vl_person}") + if direct.get("name") != vl_person: + raise RuntimeError(f"Direct Server schema check failed: {direct}") + checks.append({"name": "server-direct-schema-vertexlabel", + "status": "passed", "label": vl_person}) + return checks, { + "pk_id": pk_id, + "pk_name": pk_name, + "pk_rank": pk_rank, + "vl_person": vl_person, + "el_knows": el_knows + } + + +def encode_multipart(fields, file_field, file_name, content): + boundary = "----hubble694" + uuid.uuid4().hex + parts = [] + for name, value in fields.items(): + parts.append( + f"--{boundary}\r\n" + f"Content-Disposition: form-data; name=\"{name}\"\r\n\r\n" + f"{value}\r\n".encode("utf-8") + ) + parts.append( + f"--{boundary}\r\n" + f"Content-Disposition: form-data; name=\"{file_field}\"; " + f"filename=\"{file_name}\"\r\n" + "Content-Type: text/csv\r\n\r\n".encode("utf-8") + ) + parts.append(content) + parts.append(f"\r\n--{boundary}--\r\n".encode("utf-8")) + return b"".join(parts), f"multipart/form-data; boundary={boundary}" + + +def upload_csv(hubble_url, graph_space, graph_name, prefix, schema): + job = unwrap(request("POST", + f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/" + f"graphs/{graph_name}/job-manager", + {"job_name": f"{prefix}_job", + "job_remarks": "issue_694_live_loader_smoke"}), + "create loader job") + job_id = job["id"] + file_name = f"{prefix}_edges.csv" + csv_text = ( + f"source,target,{schema['pk_name']},{schema['pk_rank']}\n" + f"{prefix}_alice,{prefix}_bob,Alice,1\n" + f"{prefix}_bob,{prefix}_carol,Bob,2\n" + ) + token_map = unwrap(request( + "GET", + f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/job-manager/" + f"{job_id}/upload-file/token?names={urllib.parse.quote(file_name)}" + ), "create upload token") + token = token_map[file_name] + multipart_body, content_type = encode_multipart( + { + "name": file_name, + "size": str(len(csv_text.encode("utf-8"))), + "token": token, + "total": "1", + "index": "0" + }, + "file", + file_name, + csv_text.encode("utf-8") + ) + upload = unwrap(request( + "POST", + f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/job-manager/" + f"{job_id}/upload-file", + multipart_body, + {"Content-Type": content_type} + ), "upload loader csv") + file_id = upload["id"] + unwrap(request( + "PUT", + f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/job-manager/" + f"{job_id}/upload-file/next-step" + ), "advance upload step") + return job_id, file_id + + +def configure_mapping(hubble_url, graph_space, graph_name, job_id, file_id, + schema): + base = (f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/job-manager/{job_id}/file-mappings") + mapping = unwrap(request("POST", f"{base}/{file_id}/file-setting", { + "has_header": True, + "format": "CSV", + "delimiter": ",", + "charset": "UTF-8", + "date_format": "yyyy-MM-dd HH:mm:ss", + "time_zone": "GMT+8", + "skipped_line": "(^#|^//).*|" + }), "configure file setting") + if "source" not in mapping["file_setting"]["column_names"]: + raise RuntimeError(f"File setting did not read CSV columns: {mapping}") + + null_values = {"checked": ["", "NULL", "null"], "customized": []} + unwrap(request("POST", f"{base}/{file_id}/vertex-mappings", { + "label": schema["vl_person"], + "id_fields": ["source"], + "field_mapping": [ + {"column_name": schema["pk_name"], "mapped_name": schema["pk_name"]}, + {"column_name": schema["pk_rank"], "mapped_name": schema["pk_rank"]} + ], + "value_mapping": [], + "null_values": null_values + }), "add source vertex mapping") + unwrap(request("POST", f"{base}/{file_id}/vertex-mappings", { + "label": schema["vl_person"], + "id_fields": ["target"], + "field_mapping": [], + "value_mapping": [], + "null_values": null_values + }), "add target vertex mapping") + unwrap(request("POST", f"{base}/{file_id}/edge-mappings", { + "label": schema["el_knows"], + "source_fields": ["source"], + "target_fields": ["target"], + "field_mapping": [], + "value_mapping": [], + "null_values": null_values + }), "add edge mapping") + unwrap(request("PUT", f"{base}/next-step"), "advance mapping step") + unwrap(request("POST", f"{base}/load-parameter", { + "check_vertex": False, + "insert_timeout": 60, + "max_parse_errors": 1, + "max_insert_errors": 1, + "retry_times": 1, + "retry_interval": 1 + }), "configure load parameters") + + +def wait_for_load_task(hubble_url, graph_space, graph_name, job_id, file_id): + base = (f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/job-manager/{job_id}/load-tasks") + tasks = unwrap(request("POST", f"{base}/start?file_mapping_ids={file_id}", + {}), + "start load task") + if not tasks: + raise RuntimeError("Load task start returned no tasks") + task_id = tasks[0]["id"] + deadline = time.time() + 90 + last_task = tasks[0] + while time.time() < deadline: + last_task = unwrap(request("GET", f"{base}/{task_id}"), + "poll load task") + status = last_task.get("status") + if status in ("SUCCEED", "FAILED", "STOPPED", "PAUSED"): + break + time.sleep(1) + if last_task.get("status") != "SUCCEED": + raise RuntimeError(f"Load task did not succeed: {last_task}") + job = unwrap(request( + "GET", + f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/job-manager/{job_id}" + ), "poll loader job") + return task_id, last_task, job + + +def first_table_value(gremlin_result): + table = gremlin_result.get("table_view") or gremlin_result.get("tableView") + if not table: + raise RuntimeError(f"No table view in gremlin result: {gremlin_result}") + rows = table.get("data") or table.get("rows") or [] + if not rows: + raise RuntimeError(f"No rows in gremlin result: {gremlin_result}") + row = rows[0] + if isinstance(row, dict): + return next(iter(row.values())) + if isinstance(row, list): + return row[0] + return row + + +def run_hubble_gremlin(hubble_url, graph_space, graph_name, content): + return unwrap(request( + "POST", + f"{hubble_url}/api/v1.3/graphspaces/{graph_space}/graphs/" + f"{graph_name}/gremlin-query", + {"content": content} + ), f"Hubble Gremlin {content}") + + +def run_server_label_count(server_url, graph_name, resource, label): + response = server_json( + "GET", + server_url, + f"/graphs/{graph_name}/graph/{resource}" + f"?label={urllib.parse.quote(label)}&limit=100" + ) + values = response.get(resource) + if not isinstance(values, list): + raise RuntimeError(f"Unexpected Server {resource} response: {response}") + return len(values) + + +def object_id(item): + if isinstance(item, dict): + return (item.get("id") or item.get("source") or item.get("target") or + item.get("name")) + return item + + +def normalize_vertices(vertices): + return {str(object_id(vertex)) for vertex in vertices} + + +def normalize_edges(edges): + normalized = set() + for edge in edges: + if not isinstance(edge, dict): + match = re.match(r"^S(.+?)>.*>>S(.+)$", str(edge)) + if match: + normalized.add((match.group(1), match.group(2), "")) + else: + normalized.add((str(edge), "", "")) + continue + source = (edge.get("source") or edge.get("sourceId") or + edge.get("outV") or edge.get("out")) + target = (edge.get("target") or edge.get("targetId") or + edge.get("inV") or edge.get("in")) + label = edge.get("label") or edge.get("name") or "" + normalized.add((str(source), str(target), str(label))) + return normalized + + +def edge_pairs_contain(actual_edges, expected_edges): + for source, target, label in expected_edges: + if not any(edge_source == source and edge_target == target and + (edge_label in ("", label)) + for edge_source, edge_target, edge_label in actual_edges): + return False + return True + + +def run_live_function_flow(hubble_url, server_url, graph_space, graph_name, + prefix): + checks = [] + schema_checks, schema = create_schema(hubble_url, server_url, graph_space, + graph_name, prefix) + checks.extend(schema_checks) + job_id, file_id = upload_csv(hubble_url, graph_space, graph_name, prefix, + schema) + checks.append({"name": "hubble-upload-csv", "status": "passed", + "job_id": job_id, "file_mapping_id": file_id}) + configure_mapping(hubble_url, graph_space, graph_name, job_id, file_id, + schema) + checks.append({"name": "hubble-file-mapping", "status": "passed", + "file_mapping_id": file_id}) + task_id, task, job = wait_for_load_task(hubble_url, graph_space, + graph_name, job_id, file_id) + checks.append({"name": "hubble-loader-task", "status": "passed", + "task_id": task_id, "task_status": task.get("status"), + "job_status": job.get("job_status")}) + + d_vertices = run_server_label_count(server_url, graph_name, "vertices", + schema["vl_person"]) + d_edges = run_server_label_count(server_url, graph_name, "edges", + schema["el_knows"]) + expected_vertices = 3 + expected_edges = 2 + if (d_vertices, d_edges) != (expected_vertices, expected_edges): + raise RuntimeError("Loader imported unexpected graph size: " + f"{(d_vertices, d_edges)} != " + f"{(expected_vertices, expected_edges)}") + checks.append({"name": "server-loader-rest-count", + "status": "passed", "vertices": d_vertices, + "edges": d_edges, "expected_vertices": expected_vertices, + "expected_edges": expected_edges}) + + direct_shortest = server_json( + "GET", server_url, + f"/graphs/{graph_name}/traversers/shortestpath" + f"?source={urllib.parse.quote(json.dumps(prefix + '_alice'))}" + f"&target={urllib.parse.quote(json.dumps(prefix + '_carol'))}" + f"&direction=OUT&label={urllib.parse.quote(schema['el_knows'])}" + f"&max_depth=3&max_degree=1000&skip_degree=0&capacity=10000" + ) + expected_vertex_ids = { + f"{prefix}_alice", + f"{prefix}_bob", + f"{prefix}_carol" + } + expected_edge_pairs = { + (f"{prefix}_alice", f"{prefix}_bob", schema["el_knows"]), + (f"{prefix}_bob", f"{prefix}_carol", schema["el_knows"]) + } + direct_vertices = direct_shortest.get("vertices") or [] + direct_edges = direct_shortest.get("edges") or [] + direct_path = direct_shortest.get("path") or [] + direct_vertex_ids = normalize_vertices(direct_vertices) + if direct_vertex_ids and direct_vertex_ids != expected_vertex_ids: + raise RuntimeError("Server direct shortestPath vertices mismatch: " + f"{direct_vertex_ids} != {expected_vertex_ids}") + direct_edge_set = normalize_edges(direct_edges) + if direct_edge_set and not edge_pairs_contain(direct_edge_set, + expected_edge_pairs): + raise RuntimeError("Server direct shortestPath edges mismatch: " + f"{direct_edge_set} missing {expected_edge_pairs}") + if direct_path and [str(object_id(item)) for item in direct_path] != [ + f"{prefix}_alice", f"{prefix}_bob", f"{prefix}_carol"]: + raise RuntimeError(f"Server direct shortestPath path mismatch: " + f"{direct_path}") + if not direct_vertices and not direct_path: + raise RuntimeError(f"Server direct shortestPath returned no path: " + f"{direct_shortest}") + checks.append({"name": "server-direct-shortestpath", + "status": "passed", "server_vertices": len(direct_vertices), + "server_edges": len(direct_edges), + "server_path_objects": len(direct_path), + "expected_vertices": sorted(expected_vertex_ids), + "expected_edges": sorted(expected_edge_pairs)}) + checks.append({ + "name": "hubble-shortestpath-string-id-followup", + "status": "skipped", + "classification": "known-follow-up", + "reason": ("Hubble shortestPath string-id handling is tracked outside " + "the loader auth smoke gate") + }) + return checks + + +def log_tail(path): + if path is None or not path.exists(): + return None + return "\n".join(path.read_text(encoding="utf-8", errors="replace") + .splitlines()[-80:]) + + +def main(): + parser = argparse.ArgumentParser(description="Run live Hubble issue #694 smoke") + parser.add_argument("tarball", type=Path) + parser.add_argument("--server-url", default="http://127.0.0.1:8080") + parser.add_argument("--hubble-url", default=os.environ.get("HUBBLE_URL", + "http://127.0.0.1:8088")) + parser.add_argument("--graph", default=os.environ.get("HUGEGRAPH_GRAPH", "hugegraph")) + parser.add_argument("--work-dir", type=Path) + parser.add_argument("--keep-work-dir", action="store_true") + parser.add_argument("--skip-start", action="store_true") + parser.add_argument("--foreground-start", action="store_true") + parser.add_argument("--leave-running", action="store_true") + parser.add_argument("--bind-host") + parser.add_argument("--loader-flow", action="store_true") + parser.add_argument("--analysis-boundary", action="store_true") + parser.add_argument("--graphspace", default=os.environ.get("HUGEGRAPH_GRAPHSPACE", + "DEFAULT")) + parser.add_argument("--connection-name") + parser.add_argument("--data-prefix") + parser.add_argument("--json-output", type=Path) + parser.add_argument("--username", default=os.environ.get("HUBBLE_USERNAME", + "admin")) + parser.add_argument("--password", default=os.environ.get("HUBBLE_PASSWORD", + "pa")) + args = parser.parse_args() + + hubble_url = args.hubble_url.rstrip("/") + server_url = args.server_url.rstrip("/") + work_dir = args.work_dir or Path(tempfile.mkdtemp(prefix="hubble-694-live-")) + created_work_dir = args.work_dir is None + hubble_home = None + process = None + log_handle = None + hubble_log = None + app_log = None + report = { + "tarball": str(args.tarball), + "hubble_url": hubble_url, + "server_url": server_url, + "graph": args.graph, + "checks": [], + "status": "failed" + } + prefix = args.data_prefix or f"issue_694_{int(time.time())}" + connection_name = args.connection_name or f"{prefix}_conn" + report["data_prefix"] = prefix + report["connection_name"] = connection_name + + try: + if not args.skip_start: + if is_healthy(hubble_url): + raise RuntimeError("Hubble URL is already healthy before " + "candidate startup; refusing to validate an " + "unknown existing process") + work_dir.mkdir(parents=True, exist_ok=True) + hubble_home = extract_tarball(args.tarball, work_dir) + report["work_dir"] = str(work_dir) + report["hubble_home"] = str(hubble_home) + configure_hubble_endpoint(hubble_home, hubble_url, args.bind_host, + server_url) + hubble_log = work_dir / "hubble-live-smoke.log" + app_log = hubble_home / "logs" / "hugegraph-hubble.log" + log_handle = hubble_log.open("w", encoding="utf-8") + if args.foreground_start: + process = subprocess.Popen( + [str(hubble_home / "bin" / "start-hubble.sh"), "-f", "true"], + cwd=str(hubble_home), + stdout=log_handle, + stderr=subprocess.STDOUT, + text=True + ) + else: + result = subprocess.run( + [str(hubble_home / "bin" / "start-hubble.sh")], + cwd=str(hubble_home), + stdout=log_handle, + stderr=subprocess.STDOUT, + text=True, + check=False + ) + if result.returncode != 0: + raise RuntimeError("Hubble start script failed with " + f"exit code {result.returncode}") + wait_for_health(hubble_url, 90) + else: + wait_for_health(hubble_url, 10) + + report["checks"].extend(run_hubble_only_checks(hubble_url)) + report["checks"].extend(run_login_checks(hubble_url, args.username, + args.password)) + report["checks"].extend(run_server_login(server_url, args.username, + args.password)) + report["checks"].extend(run_server_checks(hubble_url, server_url, + args.graphspace, args.graph)) + + if args.loader_flow: + report["checks"].extend(run_live_function_flow(hubble_url, + server_url, + args.graphspace, + args.graph, + prefix)) + if args.analysis_boundary: + report["checks"].extend(run_analysis_boundary_checks(hubble_url, + args.graphspace, + args.graph)) + + report["status"] = "passed" + except (urllib.error.URLError, RuntimeError) as exc: + report["error"] = str(exc) + if isinstance(exc, urllib.error.HTTPError): + try: + error_payload = exc.read().decode("utf-8", errors="replace") + report["http_error_body"] = error_payload + except Exception as body_exc: # noqa: BLE001 - evidence best effort + report["http_error_body_error"] = str(body_exc) + wrapper_tail = log_tail(hubble_log) + app_tail = log_tail(app_log) + if wrapper_tail is not None: + report["hubble_wrapper_log"] = str(hubble_log) + report["hubble_wrapper_log_tail"] = wrapper_tail + if app_tail is not None: + report["hubble_app_log"] = str(app_log) + report["hubble_app_log_tail"] = app_tail + raise SystemExit(json.dumps(report, indent=2, sort_keys=True)) + finally: + if process is not None and not args.leave_running: + process.terminate() + try: + process.wait(timeout=20) + except subprocess.TimeoutExpired: + process.kill() + if log_handle is not None: + log_handle.close() + if hubble_home is not None and not args.leave_running: + subprocess.run([str(hubble_home / "bin" / "stop-hubble.sh")], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False) + if args.json_output: + args.json_output.parent.mkdir(parents=True, exist_ok=True) + args.json_output.write_text(json.dumps(report, indent=2, + sort_keys=True) + "\n", + encoding="utf-8") + if created_work_dir and not args.keep_work_dir and not args.leave_running: + shutil.rmtree(work_dir, ignore_errors=True) + + print(json.dumps(report, indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js new file mode 100755 index 000000000..1429578b4 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js @@ -0,0 +1,215 @@ +#!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +'use strict'; + +const fs = require('fs'); +const moduleBuiltin = require('module'); +const path = require('path'); +const {authenticateUi} = require('./ui_auth'); + +const MAC_CHROME_PATHS = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing', + path.join(process.env.HOME || '', + 'Library/Caches/ms-playwright/chromium-1226/chrome-mac-arm64/' + + 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing') +]; + +function argValue(name, fallback) { + const index = process.argv.indexOf(name); + if (index >= 0 && index + 1 < process.argv.length) { + return process.argv[index + 1]; + } + return fallback; +} + +function chromiumExecutablePath() { + const configured = argValue('--chromium-executable', + process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || + process.env.CHROME_PATH || ''); + if (configured) { + return configured; + } + for (const candidate of MAC_CHROME_PATHS) { + if (candidate && fs.existsSync(candidate)) { + return candidate; + } + } + return undefined; +} + +async function loadPlaywright() { + const hubbleRoot = path.resolve(__dirname, '../../..'); + const candidateModules = [ + process.env.PLAYWRIGHT_NODE_MODULES || '', + path.join(hubbleRoot, 'hubble-fe', 'node_modules') + ].filter(Boolean); + try { + return require('playwright'); + } catch (error) { + for (const nodeModules of candidateModules) { + try { + const requireFrom = moduleBuiltin.createRequire( + path.join(nodeModules, 'playwright', 'package.json') + ); + return requireFrom('playwright'); + } catch (_) { + // Continue to the next configured module root. + } + } + throw new Error( + 'Playwright is required for UI browser smoke. Install/enable it before ' + + 'closing the browser gate, or set PLAYWRIGHT_NODE_MODULES. Original ' + + 'error: ' + error.message + ); + } +} + +async function main() { + const hubbleUrl = (argValue('--hubble-url', process.env.HUBBLE_URL) || + 'http://127.0.0.1:8088').replace(/\/$/, ''); + const outputDir = path.resolve(argValue('--output-dir', + '.workflow/hubble-v2-issue-694/evidence/ui')); + const connId = argValue('--conn-id', process.env.HUBBLE_CONN_ID || '1'); + const username = argValue('--username', process.env.HUBBLE_USERNAME || 'admin'); + const password = argValue('--password', process.env.HUBBLE_PASSWORD || 'pa'); + const jsonOutput = argValue('--json-output', ''); + const { chromium } = await loadPlaywright(); + const executablePath = chromiumExecutablePath(); + + fs.mkdirSync(outputDir, { recursive: true }); + const browser = await chromium.launch({ headless: true, executablePath }); + const context = await browser.newContext(); + const page = await context.newPage({ viewport: { width: 1440, height: 900 } }); + const network = []; + const consoleErrors = []; + + await page.addInitScript(() => { + window.localStorage.setItem('languageType', 'zh-CN'); + }); + const auth = await authenticateUi(context, page, hubbleUrl, username, password); + + page.on('response', async (response) => { + const request = response.request(); + const url = response.url(); + if (url.includes('/api/v1.3/')) { + let businessStatus = null; + try { + const body = await response.json(); + businessStatus = body && body.status !== undefined ? body.status : null; + } catch (_) { + // Non-JSON API responses are still represented by HTTP status. + } + network.push({ + method: request.method(), + url, + httpStatus: response.status(), + ok: response.ok(), + businessStatus + }); + } + }); + page.on('console', (message) => { + if (message.type() === 'error') { + consoleErrors.push(message.text()); + } + }); + + const routes = [ + { name: 'graphspace', path: '/graphspace', + requiredApis: ['/api/v1.3/graphspaces'], + textPattern: /图空间|Graph Space/ }, + { name: 'gremlin', path: '/gremlin', + requiredApis: ['/api/v1.3/graphspaces/list'], + textPattern: /Gremlin|图查询|查询/ }, + { name: 'algorithms', path: '/algorithms', + requiredApis: ['/api/v1.3/graphspaces/list'], + textPattern: /算法|Algorithm|OLTP|OLAP/ }, + { name: 'asyncTasks', path: '/asyncTasks', + requiredApis: ['/api/v1.3/graphspaces/list'], + textPattern: /异步|Async|Task|任务/ } + ]; + + const results = []; + try { + for (const route of routes) { + network.length = 0; + await page.goto(hubbleUrl + route.path, { + waitUntil: 'networkidle', + timeout: 30000 + }); + await page.waitForTimeout(500); + const screenshot = path.join(outputDir, `${route.name}.png`); + await page.screenshot({ path: screenshot, fullPage: true }); + const text = await page.locator('body').innerText({ timeout: 5000 }); + const rawKeyPattern = new RegExp( + '\\b(addition|analysis|async-tasks|common|home|manage|navigation|' + + 'server-data-import|Topbar)\\.[A-Za-z0-9_.-]+' + ); + const matchedApis = route.requiredApis.map((requiredApi) => { + const entries = network.filter((entry) => entry.url.includes(requiredApi)); + return { + requiredApi, + entries, + passed: entries.some((entry) => entry.ok && + (entry.businessStatus === null || + entry.businessStatus === 200)) + }; + }); + results.push({ + route: route.path, + screenshot, + matchedApis, + rawI18nKeyFound: rawKeyPattern.test(text), + routeTextMatched: route.textPattern.test(text), + notFoundPage: /404|页面不存在|Not Found/.test(text), + requestCount: network.length + }); + } + } finally { + await browser.close(); + } + + const report = { + hubbleUrl, + authenticatedUser: auth.user.user_name, + authLevel: auth.level, + results, + consoleErrors, + status: results.every((result) => ( + result.matchedApis.every((api) => api.passed) && + result.routeTextMatched && + !result.rawI18nKeyFound && + !result.notFoundPage + )) && consoleErrors.length === 0 ? 'passed' : 'failed' + }; + if (jsonOutput) { + fs.mkdirSync(path.dirname(path.resolve(jsonOutput)), { recursive: true }); + fs.writeFileSync(jsonOutput, JSON.stringify(report, null, 2) + '\n'); + } + console.log(JSON.stringify(report, null, 2)); + if (report.status !== 'passed') { + process.exit(1); + } +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_full_acceptance.js b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_full_acceptance.js new file mode 100755 index 000000000..34a69a044 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_full_acceptance.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +'use strict'; + +const childProcess = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +function argValue(name, fallback) { + const index = process.argv.indexOf(name); + if (index >= 0 && index + 1 < process.argv.length) { + return process.argv[index + 1]; + } + return fallback; +} + +function run(name, command, args) { + const startedAt = new Date().toISOString(); + const result = childProcess.spawnSync(command, args, { + cwd: path.resolve(__dirname, '../../../..'), + encoding: 'utf-8' + }); + return { + name, + command: [command, ...args].join(' '), + startedAt, + status: result.status === 0 ? 'passed' : 'failed', + stdout: result.stdout, + stderr: result.stderr, + error: result.error ? result.error.message : undefined + }; +} + +function main() { + const scriptDir = __dirname; + const outputDir = path.resolve(argValue('--output-dir', + '.workflow/hubble-v2-issue-694/evidence/ui')); + const hubbleUrl = argValue('--hubble-url', process.env.HUBBLE_URL || + 'http://127.0.0.1:8088'); + const connId = argValue('--conn-id', process.env.HUBBLE_CONN_ID || '1'); + const chromiumExecutable = argValue('--chromium-executable', + process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || + process.env.CHROME_PATH || ''); + const jsonOutput = argValue('--json-output', + path.join(outputDir, 'ui-full-acceptance.json')); + fs.mkdirSync(outputDir, { recursive: true }); + + const checks = [ + run('ui-browser-smoke', process.execPath, [ + path.join(scriptDir, 'run_ui_browser_smoke.js'), + '--hubble-url', hubbleUrl, + '--conn-id', connId, + '--output-dir', outputDir, + '--json-output', path.join(outputDir, 'ui-browser-smoke.json'), + '--chromium-executable', chromiumExecutable + ]), + run('ui-i18n-switch-smoke', process.execPath, [ + path.join(scriptDir, 'run_ui_i18n_switch_smoke.js'), + '--hubble-url', hubbleUrl, + '--output-dir', outputDir, + '--json-output', path.join(outputDir, 'ui-i18n-switch-smoke.json'), + '--chromium-executable', chromiumExecutable + ]) + ]; + + const report = { + hubbleUrl, + connId, + outputDir, + checks, + status: checks.every((check) => check.status === 'passed') ? 'passed' : 'failed' + }; + fs.writeFileSync(path.resolve(jsonOutput), JSON.stringify(report, null, 2) + '\n'); + console.log(JSON.stringify(report, null, 2)); + if (report.status !== 'passed') { + process.exit(1); + } +} + +main(); diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_i18n_switch_smoke.js b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_i18n_switch_smoke.js new file mode 100755 index 000000000..ca940b2fa --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_i18n_switch_smoke.js @@ -0,0 +1,178 @@ +#!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +'use strict'; + +const fs = require('fs'); +const moduleBuiltin = require('module'); +const path = require('path'); +const {authenticateUi} = require('./ui_auth'); + +const MAC_CHROME_PATHS = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing', + path.join(process.env.HOME || '', + 'Library/Caches/ms-playwright/chromium-1226/chrome-mac-arm64/' + + 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing') +]; + +function argValue(name, fallback) { + const index = process.argv.indexOf(name); + if (index >= 0 && index + 1 < process.argv.length) { + return process.argv[index + 1]; + } + return fallback; +} + +function chromiumExecutablePath() { + const configured = argValue('--chromium-executable', + process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || + process.env.CHROME_PATH || ''); + if (configured) { + return configured; + } + for (const candidate of MAC_CHROME_PATHS) { + if (candidate && fs.existsSync(candidate)) { + return candidate; + } + } + return undefined; +} + +async function loadPlaywright() { + const hubbleRoot = path.resolve(__dirname, '../../..'); + const candidateModules = [ + process.env.PLAYWRIGHT_NODE_MODULES || '', + path.join(hubbleRoot, 'hubble-fe', 'node_modules') + ].filter(Boolean); + try { + return require('playwright'); + } catch (error) { + for (const nodeModules of candidateModules) { + try { + const requireFrom = moduleBuiltin.createRequire( + path.join(nodeModules, 'playwright', 'package.json') + ); + return requireFrom('playwright'); + } catch (_) { + // Continue to the next configured module root. + } + } + throw new Error( + 'Playwright is required for runtime i18n smoke. Install/enable it or ' + + 'set PLAYWRIGHT_NODE_MODULES. Original error: ' + error.message + ); + } +} + +async function captureLanguage(page, hubbleUrl, screenshot) { + await page.goto(hubbleUrl + '/graphspace', { + waitUntil: 'networkidle', + timeout: 30000 + }); + await page.waitForTimeout(500); + await page.screenshot({ path: screenshot, fullPage: true }); + return await page.locator('body').innerText({ timeout: 5000 }); +} + +async function switchToEnglish(page) { + await page.locator('.ant-layout-header .ant-select-selector') + .click({ timeout: 5000 }); + await page.locator('.ant-select-item-option[title="English"]') + .click({ timeout: 5000 }); + await page.waitForFunction( + () => window.localStorage.getItem('languageType') === 'en-US', + null, + { timeout: 5000 } + ); + await page.waitForLoadState('networkidle', { timeout: 30000 }); + await page.waitForTimeout(500); +} + +async function main() { + const hubbleUrl = (argValue('--hubble-url', process.env.HUBBLE_URL) || + 'http://127.0.0.1:8088').replace(/\/$/, ''); + const outputDir = path.resolve(argValue('--output-dir', + '.workflow/hubble-v2-issue-694/evidence/ui')); + const jsonOutput = argValue('--json-output', ''); + const username = argValue('--username', process.env.HUBBLE_USERNAME || 'admin'); + const password = argValue('--password', process.env.HUBBLE_PASSWORD || 'pa'); + const { chromium } = await loadPlaywright(); + const executablePath = chromiumExecutablePath(); + fs.mkdirSync(outputDir, { recursive: true }); + + const browser = await chromium.launch({ headless: true, executablePath }); + const context = await browser.newContext(); + const page = await context.newPage({ viewport: { width: 1440, height: 900 } }); + await page.addInitScript(() => { + if (!window.localStorage.getItem('languageType')) { + window.localStorage.setItem('languageType', 'zh-CN'); + } + }); + const auth = await authenticateUi(context, page, hubbleUrl, username, password); + let zhText; + let enText; + let selectorTextAfterSwitch = ''; + try { + zhText = await captureLanguage(page, hubbleUrl, + path.join(outputDir, 'i18n-zh-CN.png')); + await switchToEnglish(page); + selectorTextAfterSwitch = await page.locator('.ant-select').first() + .innerText({ timeout: 5000 }); + await page.screenshot({ + path: path.join(outputDir, 'i18n-en-US.png'), + fullPage: true + }); + enText = await page.locator('body').innerText({ timeout: 5000 }); + } finally { + await browser.close(); + } + + const rawKeyPattern = new RegExp( + '\\b(addition|analysis|async-tasks|common|home|manage|navigation|' + + 'server-data-import|Topbar)\\.[A-Za-z0-9_.-]+' + ); + const report = { + hubbleUrl, + authenticatedUser: auth.user.user_name, + authLevel: auth.level, + zhContainsChinese: /[\u4e00-\u9fff]/.test(zhText), + enSelectorVisible: /English/.test(selectorTextAfterSwitch), + textChanged: zhText !== enText, + rawI18nKeyFound: rawKeyPattern.test(zhText) || rawKeyPattern.test(enText), + notFoundPage: /404|页面不存在|Not Found/.test(zhText + enText), + status: 'failed' + }; + report.status = report.zhContainsChinese && report.enSelectorVisible && + report.textChanged && !report.rawI18nKeyFound && + !report.notFoundPage ? 'passed' : 'failed'; + + if (jsonOutput) { + fs.mkdirSync(path.dirname(path.resolve(jsonOutput)), { recursive: true }); + fs.writeFileSync(jsonOutput, JSON.stringify(report, null, 2) + '\n'); + } + console.log(JSON.stringify(report, null, 2)); + if (report.status !== 'passed') { + process.exit(1); + } +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh b/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh index efbfbd339..d47268d7c 100644 --- a/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh +++ b/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh @@ -33,6 +33,7 @@ PID_FILE=${BIN_PATH}/pid print_usage() { echo " usage: start-hubble.sh [options]" echo " options: " + echo " -f,--foreground Start program in foreground mode" echo " -d,--debug Start program in debug mode" echo " -h,--help Display help information" } @@ -50,12 +51,16 @@ for jar in "${LIB_PATH}"/*.jar; do done java_opts="-Xms512m" +foreground="false" while [[ $# -gt 0 ]]; do case $1 in --help|-help|-h) print_usage exit 0 ;; + --foreground|-f) + foreground="true" + ;; --debug|-d) java_opts="$java_opts -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=y" ;; @@ -80,7 +85,11 @@ args=${CONF_PATH}/hugegraph-hubble.properties log=${LOG_PATH}/hugegraph-hubble.log echo -n "starting HugeGraphHubble " -nohup nice -n 0 java -server -Dfile.encoding=UTF-8 "${java_opts}" "${agent_opts}" -Dhubble.home.path="${HOME_PATH}" -cp "${class_path}" ${main_class} "${args}" > "${log}" 2>&1 < /dev/null & +if [[ ${foreground} == "false" ]]; then + nohup nice -n 0 java -server -Dfile.encoding=UTF-8 "${java_opts}" "${agent_opts}" -Dhubble.home.path="${HOME_PATH}" -cp "${class_path}" ${main_class} "${args}" > "${log}" 2>&1 < /dev/null & +else + exec nice -n 0 java -server -Dfile.encoding=UTF-8 "${java_opts}" "${agent_opts}" -Dhubble.home.path="${HOME_PATH}" -cp "${class_path}" ${main_class} "${args}" +fi pid=$! echo ${pid} > "${PID_FILE}" diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py b/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py new file mode 100644 index 000000000..945cc9539 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +import importlib.util +import unittest +from pathlib import Path +from unittest import mock + + +SCRIPT = Path(__file__).with_name("run_live_hubble_smoke.py") +SPEC = importlib.util.spec_from_file_location("run_live_hubble_smoke", SCRIPT) +SMOKE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(SMOKE) + + +class HubbleReadinessTest(unittest.TestCase): + + def test_readiness_rejects_spa_html(self): + self.assertFalse(SMOKE.is_hubble_readiness_response( + '
')) + + def test_readiness_requires_about_response_shape(self): + self.assertFalse(SMOKE.is_hubble_readiness_response( + {"status": 200, "data": {"name": "HugeGraph-Hubble"}})) + self.assertTrue(SMOKE.is_hubble_readiness_response({ + "status": 200, + "data": {"name": "HugeGraph-Hubble", "version": "3.0.0"} + })) + + @mock.patch.object(SMOKE, "request") + def test_preflight_uses_strict_about_api(self, request): + request.return_value = '
' + + self.assertFalse(SMOKE.is_healthy("http://127.0.0.1:8088")) + request.assert_called_once_with( + "GET", "http://127.0.0.1:8088/about", timeout=2) + + +class AnalysisBoundaryTest(unittest.TestCase): + + @mock.patch.object(SMOKE, "get_json_status", side_effect=[400, 400]) + def test_environment_dependent_boundaries_are_skipped(self, _status): + checks = SMOKE.run_analysis_boundary_checks( + "http://127.0.0.1:8088", "DEFAULT", "hugegraph") + + self.assertEqual(["skipped", "skipped"], + [check["status"] for check in checks]) + self.assertEqual([400, 400], + [check["http_or_business_status"] + for check in checks]) + + @mock.patch.object(SMOKE, "get_json_status", side_effect=[200, 503]) + def test_service_failure_is_not_skipped(self, _status): + with self.assertRaisesRegex(RuntimeError, + "analysis-olap-boundary failed: status 503"): + SMOKE.run_analysis_boundary_checks( + "http://127.0.0.1:8088", "DEFAULT", "hugegraph") + + @mock.patch.object(SMOKE, "get_json_status", return_value=200) + def test_available_boundaries_pass(self, _status): + checks = SMOKE.run_analysis_boundary_checks( + "http://127.0.0.1:8088", "DEFAULT", "hugegraph") + + self.assertEqual(["passed", "passed"], + [check["status"] for check in checks]) + + +if __name__ == "__main__": + unittest.main() diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.js b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.js new file mode 100644 index 000000000..33a6889d7 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.js @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +'use strict'; + +async function payload(response, name) { + let body; + try { + body = await response.json(); + } catch (error) { + throw new Error(`${name} returned unreadable JSON: ${error.message}`); + } + if (!response.ok()) { + throw new Error(`${name} failed: HTTP ${response.status()}`); + } + if (!body || body.status !== 200) { + const detail = body && typeof body.message === 'string' && body.message.trim(); + throw new Error(`${name} failed: ${detail || `business status ${body?.status}`}`); + } + return body.data; +} + +async function authenticateUi(context, page, hubbleUrl, username, password) { + const baseUrl = hubbleUrl.replace(/\/$/, ''); + const user = await payload(await context.request.post( + `${baseUrl}/api/v1.3/auth/login`, + {data: {user_name: username, user_password: password}} + ), 'login'); + const status = await payload(await context.request.get( + `${baseUrl}/api/v1.3/auth/status` + ), 'auth status'); + if (!status || !status.level) { + throw new Error('auth status response did not include level'); + } + if (!user || !user.user_name) { + throw new Error('login response did not include user_name'); + } + await page.addInitScript(serverUser => { + window.sessionStorage.setItem('user_', JSON.stringify(serverUser)); + }, user); + return {user, level: status.level}; +} + +module.exports = {authenticateUi}; diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.test.js b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.test.js new file mode 100644 index 000000000..4d8ca29d1 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.test.js @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +'use strict'; + +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const {authenticateUi} = require('./ui_auth'); + +function response(httpStatus, body) { + return { + ok: () => httpStatus >= 200 && httpStatus < 300, + status: () => httpStatus, + json: async () => body + }; +} + +function fixture(responses) { + const calls = []; + const initScripts = []; + return { + context: { + request: { + post: async (url, options) => { + calls.push({method: 'POST', url, options}); + return responses.shift(); + }, + get: async url => { + calls.push({method: 'GET', url}); + return responses.shift(); + } + } + }, + page: { + addInitScript: async (fn, value) => initScripts.push({fn, value}) + }, + calls, + initScripts + }; +} + +test('rejects an HTTP 401 login response', async () => { + const item = fixture([response(401, {status: 401})]); + await assert.rejects( + authenticateUi(item.context, item.page, 'http://hubble', 'admin', 'bad'), + /login failed: HTTP 401/ + ); +}); + +test('rejects a non-success login business status', async () => { + const item = fixture([response(200, {status: 401, message: 'denied'})]); + await assert.rejects( + authenticateUi(item.context, item.page, 'http://hubble', 'admin', 'bad'), + /login failed: denied/ + ); +}); + +test('requires an authenticated status with a level', async () => { + const item = fixture([ + response(200, {status: 200, data: {user_name: 'admin'}}), + response(200, {status: 200, data: {}}) + ]); + await assert.rejects( + authenticateUi(item.context, item.page, 'http://hubble', 'admin', 'pa'), + /auth status response did not include level/ + ); +}); + +test('uses the server user and shared browser context session', async () => { + const user = {id: 7, user_name: 'admin', is_superadmin: true}; + const item = fixture([ + response(200, {status: 200, data: user}), + response(200, {status: 200, data: {level: 'ADMIN'}}) + ]); + const result = await authenticateUi(item.context, item.page, + 'http://hubble/', 'admin', 'pa'); + + assert.deepEqual(result, {user, level: 'ADMIN'}); + assert.equal(item.calls[0].url, 'http://hubble/api/v1.3/auth/login'); + assert.deepEqual(item.calls[0].options.data, { + user_name: 'admin', + user_password: 'pa' + }); + assert.equal(item.calls[1].url, 'http://hubble/api/v1.3/auth/status'); + assert.equal(item.initScripts.length, 1); + assert.deepEqual(item.initScripts[0].value, user); +}); diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/verify-hubble-issue-694.sh b/hugegraph-hubble/hubble-dist/assembly/travis/verify-hubble-issue-694.sh new file mode 100755 index 000000000..2f23d8563 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/verify-hubble-issue-694.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +set -euo pipefail + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 [server-url]" >&2 + exit 1 +fi + +tarball=$1 +server_url=${2:-http://127.0.0.1:8080} +hubble_url=${HUBBLE_URL:-http://127.0.0.1:8088} +graph_name=${HUGEGRAPH_GRAPH:-hugegraph} +graphspace=${HUGEGRAPH_GRAPHSPACE:-DEFAULT} +evidence_dir=${HUBBLE_694_EVIDENCE_DIR:-.workflow/hubble-v2-issue-694/evidence} +script_dir=$(cd "$(dirname "$0")" && pwd) +python_bin=${PYTHON:-python3} +node_bin=${NODE:-node} +username=${HUBBLE_USERNAME:-admin} +password=${HUBBLE_PASSWORD:-pa} + +if [[ ! -f "${tarball}" ]]; then + echo "Hubble tarball not found: ${tarball}" >&2 + exit 1 +fi + +mkdir -p "${evidence_dir}/ui" +runtime_work=$(mktemp -d "${TMPDIR:-/tmp}/hubble-694-runtime-XXXXXX") +cleanup() { + hubble_home=$(find "${runtime_work}" -maxdepth 1 -type d \ + -name 'apache-hugegraph-hubble-*' | head -n 1) + if [[ -n "${hubble_home}" && -x "${hubble_home}/bin/stop-hubble.sh" ]]; then + "${hubble_home}/bin/stop-hubble.sh" >/dev/null 2>&1 || true + fi + mkdir -p "${evidence_dir}/logs" + if [[ -f "${runtime_work}/hubble-live-smoke.log" ]]; then + cp "${runtime_work}/hubble-live-smoke.log" \ + "${evidence_dir}/logs/hubble-live-smoke.log" + fi + if [[ -n "${hubble_home}" && \ + -f "${hubble_home}/logs/hugegraph-hubble.log" ]]; then + cp "${hubble_home}/logs/hugegraph-hubble.log" \ + "${evidence_dir}/logs/hugegraph-hubble.log" + fi + rm -rf "${runtime_work}" +} +trap cleanup EXIT + +"${python_bin}" "${script_dir}/run_live_hubble_smoke.py" "${tarball}" \ + --server-url "${server_url}" \ + --hubble-url "${hubble_url}" \ + --graph "${graph_name}" \ + --graphspace "${graphspace}" \ + --loader-flow \ + --analysis-boundary \ + --work-dir "${runtime_work}" \ + --keep-work-dir \ + --leave-running \ + --json-output "${evidence_dir}/live-hubble-smoke.json" + +HUBBLE_USERNAME="${username}" HUBBLE_PASSWORD="${password}" \ +"${node_bin}" "${script_dir}/run_ui_full_acceptance.js" \ + --hubble-url "${hubble_url}" \ + --output-dir "${evidence_dir}/ui" \ + --json-output "${evidence_dir}/ui-full-acceptance.json" + +cat > "${evidence_dir}/manifest.json" <${project.parent.artifactId} apache-${release.name}-${project.version} ${project.basedir}/.. + ${top.level.dir}/.. ${project.basedir} ${current.basedir}/assembly ${assembly.dir}/descriptor ${assembly.dir}/static + ${repo.root.dir}/hugegraph-dist/release-docs ${project.basedir}/../hubble-fe - v16.20.2 + v18.20.8 v1.22.21 @@ -43,6 +45,19 @@ org.apache.hugegraph hubble-be ${revision} + + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.springframework.boot + spring-boot-starter-log4j2 + + @@ -88,9 +103,6 @@ ${build.node.version} ${build.yarn.version} - https://mirrors.aliyun.com/nodejs-release/ - https://repo.huaweicloud.com/yarn/ - https://registry.npmmirror.com @@ -100,7 +112,7 @@ yarn - install --network-timeout 600000 + install --frozen-lockfile --network-timeout 600000 --prefer-offline --ignore-engines @@ -111,6 +123,10 @@ build + + true + --max-old-space-size=4096 + @@ -128,13 +144,23 @@ cd ${hubble-fe.dir} || exit 1 - export CI=false + export CI=true cd ${top.level.dir} && pwd rm -rf ${final.name}/ui cp -r ${hubble-fe.dir}/build ${final.name}/ui + rm -rf ${final.name}/logs + rm -rf ${final.name}/upload-files + rm -f ${final.name}/db.mv.db + rm -f ${final.name}/db.trace.db + rm -f ${final.name}/*.pid + rm -f ${final.name}/bin/pid + find ${final.name}/ui -name "*.map" -type f -delete tar -zcvf ${top.level.dir}/target/${final.name}.tar.gz ${final.name} || exit 1 + bash ${assembly.dir}/travis/check-hubble-dist.sh \ + ${top.level.dir}/target/${final.name}.tar.gz || exit 1 + rm -rf ./hubble-dist/${final.name} cp -r ${final.name} ./hubble-dist echo -n "${final.name} tar.gz available at: " echo "${top.level.dir}/target/${final.name}.tar.gz" diff --git a/hugegraph-hubble/hubble-fe/.env b/hugegraph-hubble/hubble-fe/.env deleted file mode 100644 index 595d7e6d7..000000000 --- a/hugegraph-hubble/hubble-fe/.env +++ /dev/null @@ -1,18 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with this -# work for additional information regarding copyright ownership. The ASF -# licenses this file to You 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. - -SKIP_PREFLIGHT_CHECK=true -INLINE_RUNTIME_CHUNK=false -GENERATE_SOURCEMAP=false diff --git a/hugegraph-hubble/hubble-fe/.eslintrc.js b/hugegraph-hubble/hubble-fe/.eslintrc.js new file mode 100644 index 000000000..7f9cb9783 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/.eslintrc.js @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +module.exports = { + 'env': { + 'browser': true, + 'es2021': true, + }, + 'extends': [ + // "eslint:recommended", + // "plugin:react/recommended", + // "plugin:@typescript-eslint/recommended", + + '@ecomfe/eslint-config/baidu/default', // 根据代码库ES版本选择default或es5 + '@ecomfe/eslint-config/baidu/defect', // 根据代码库ES版本选择defect或defect-es5 + '@ecomfe/eslint-config', + '@ecomfe/eslint-config/typescript', + '@ecomfe/eslint-config/react', + ], + 'parser': '@typescript-eslint/parser', + 'parserOptions': { + 'ecmaFeatures': { + 'jsx': true, + }, + 'ecmaVersion': 12, + 'sourceType': 'module', + }, + 'plugins': [ + 'react', + '@typescript-eslint', + ], + 'overrides': [ + { + 'files': ['*.ts', '*.tsx'], + 'parserOptions': { + 'project': './tsconfig.eslint.json', + 'tsconfigRootDir': __dirname, + }, + }, + ], + 'rules': { + }, +}; diff --git a/hugegraph-hubble/hubble-fe/CHANGELOG.md b/hugegraph-hubble/hubble-fe/CHANGELOG.md deleted file mode 100644 index 0db38a1c7..000000000 --- a/hugegraph-hubble/hubble-fe/CHANGELOG.md +++ /dev/null @@ -1,76 +0,0 @@ -## 1.6.0 (2020-08-14) - -#### :rocket: New Feature - -- Add analyzing algorithm tab in data-analyze module, only support shortest-path algorithm for now - -
- -## 1.5.0 (2020-08-06) - -#### :rocket: New Feature - -- Add async task manager -- Separate Gremlin query and task in data-analyze module, users can move to task details directly in history -- Display `deleting` status in schema configurations - -
- -## 1.3.1 (2020-07-28) - -1.3.1 added support for data import from users by uploading their .csv files and setting data mappings. - -Users can create jobs and manage load tasks with each. Details of Jobs that failed or succeed can be checked out. - -#### :rocket: New Feature - -- data-import support -- Job manager can handle different jobs, which could contain multiple load tasks - -
- -## 1.2.1 (2020-02-27) - -1.2.1 added graph-mode view for metadata management. It supports all functions that list-mode has with better visual effect. - -#### :rocket: New Feature - -- graph-mode view in metadata management - -
- -## 2019-08-16 - -### Graph Management (front page) - -#### :nail_care: Enhancement: - -- Hover on text in GraphManamgentList will pop complete text - -
- -## 2019-08-15 - -### Graph Management (front page) - -#### :bug: Bug Fix: - -- Unexpected word break on navbar which happened in Safari -- Layout didn't vanish after the success request of creating/editing graph -- Empty value in username/password field would cause an error in front-end after click 'save' button -- Unexpected radio of width & word break in graph-list -- Hover & active state of primary button didn't show up - -#### :nail_care: Enhancement: - -- Change placeholders of username/password field in graph-creation form -- Change text in delete modal (confirmation to delete a graph) -- Change styles of title in Modal Component -- Normal/hover/active/disabled state of primary button now has a new series of background colors -- Fix the incorrect color of text in the left side of GraphManagementHeader -- Disable button for graph-creation before user closes the layout -- Add validation for host/port in form -- Cancel the linkage between highlighted words in list and values in search, the matched words should be highlighted after a search request -- GraphManagementList will reset to default if user clicks the reset button in search, and he has already searched a word before (which equals to refresh the page) -- Replace the favico with a new one. - diff --git a/hugegraph-hubble/hubble-fe/CHANGELOG.zh-CN.md b/hugegraph-hubble/hubble-fe/CHANGELOG.zh-CN.md deleted file mode 100644 index feecb38a1..000000000 --- a/hugegraph-hubble/hubble-fe/CHANGELOG.zh-CN.md +++ /dev/null @@ -1,133 +0,0 @@ -## 1.6.0 (2020-08-14) - -#### :rocket: 特性 - -- 数据分析模块添加算法分析,算法目前仅支持最短路径 - -
- -## 1.5.0 (2020-08-06) - -#### :rocket: 特性 - -- 添加异步任务管理模块 -- 数据分析添加执行任务选项,历史记录区分 Gremlin 查询与 Gremlin 任务,后者可通过详情跳转至异步任务管理模块 -- 删除元数据会显示删除中状态 - -
- -## 1.3.1 (2020-07-28) - -1.3.1 添加了数据导入支持,用户可通过上传 .csv 文件来导入相应的数据并设置数据映射 - -用户可创建任务并管理相应的导入任务,成功或失败的任务可查看详情 - -#### :rocket: 特性 - -- 数据导入支持 -- 任务管理可处理多个任务,每个任务可包含多个导入任务 - -
- -## 1.2.1 (2020-02-27) - -1.2.1 在元数据管理模块添加图模式,包含列表模式的大部分功能,且有着更好的视觉效果 - -#### :rocket: 特性 - -- 元数据管理添加图模式 - -
- -## 2019-09-04 - -### 图管理(数据分析) - -#### :rocket: 特性 - -- 执行语句后可通过表格形式查看返回结果 -- 执行语句后可通过 JSON 形式查看返回结果 - -
- -## 2019-09-03 - -### 团管理(数据分析) - -#### :bug: 修复: - -- 执行/收藏按钮 Tooltip 显示错误(#38) -- 执行语句(代码编辑器)部分出现额外滚动条(#39) -- 执行语句请求成功未退出弹窗(#40) -- 执行语句后代码编辑器未退出 readOnly 状态(#41) - -
- -## 2019-09-02 - -### 图管理(首页) - -#### :bug: 修复: - -- 新建表单点击右上关闭与点击取消按钮逻辑不同(#31) - -#### :nail_care: 改进: - -- 导航栏点击图片和 tab,跳转到首页或对应 url - -
- - -## 2019-08-21 - -### 图管理(首页) - -#### :bug: 修复: - -- 点击分页无效 -- 表单验证失败,退出重进表单页面,失败状态未消失 -- Dropdown 下拉菜单在 Modal 蒙层上方并可点击多次 -- 编辑搜索结果卡片并保存,请求未带搜索关键字参数 - -#### :nail_care: 改进: - -- Dropdown 下拉菜单文字居中(手动) -- Message 弹出消息去除退出 icon,左侧图标与文字对齐 - -
- -## 2019-08-16 - -### 图管理(首页) - -#### :nail_care: 改进: - -- 卡片列表鼠标悬浮在内容上会出现完整文本,以配合文字省略 - -
- -## 2019-08-15 - -### 图管理(首页) - -#### :bug: 修复: - -- Safari 上导航栏文字折行 -- 创建/编辑图,点击创建或保存按钮后请求成功,面板未消失 -- 编辑图,若用户名密码未填写,页面报错 -- 卡片列表未对齐,且有折行现象 -- 蓝色按钮 hover,active 状态未显现 - -#### :nail_care: 改进: - -- 修改创建图中用户名和密码文案 -- 修改删除卡片 Modal 中的文案 -- 修改删除卡片的 title 样式 -- 修改蓝色按钮四种状态下的颜色 -- 修改卡片列表顶部左侧文字(图管理)颜色 -- 点击创建图按钮,退出创建图面板之前该按钮不可用 -- 创建/编辑图表单,添加主机名和端口号的校验 -- 搜索文本与列表文本的高亮联动,由输入搜索内容实施高亮列表卡片中的文本改为点击搜索之后再高亮对应搜索文字 -- 点击搜索框中的清除文本按钮,如果之前已搜索相关内容,卡片列表则恢复到初始状态(类同于页面刷新) -- 替换标签页 favico - diff --git a/hugegraph-hubble/hubble-fe/README.md b/hugegraph-hubble/hubble-fe/README.md index ab02f7df3..58beeaccd 100644 --- a/hugegraph-hubble/hubble-fe/README.md +++ b/hugegraph-hubble/hubble-fe/README.md @@ -1,3 +1,70 @@ -# HugeGraph-Hubble +# Getting Started with Create React App -Front-end implementation of HugeGraph \ No newline at end of file +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes.\ +You may also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can't go back!** + +If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. + +You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `npm run build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/hugegraph-hubble/hubble-fe/add-license.js b/hugegraph-hubble/hubble-fe/add-license.js deleted file mode 100644 index b0acf6616..000000000 --- a/hugegraph-hubble/hubble-fe/add-license.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -var fs = require('fs') -var path = require('path') -const license = `/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements; and to You under the Apache License, Version 2.0. - */ -` -const license_html = `` - -const full_license = `/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ -` -// str 以 key 结尾 -function _endWith(str, key){ - var reg = new RegExp(key + '$') - return reg.test(str) -} -function walkSync(currentDirPath, callback){ - fs.readdirSync(currentDirPath, { withFileTypes: true }).forEach(function(dirent) { - var filePath = path.join(currentDirPath, dirent.name) - if (dirent.isFile()) { - callback(filePath, dirent) - } else if (dirent.isDirectory()) { - walkSync(filePath, callback) - } - }); -} -walkSync('build', function(a, b) { - if (_endWith(b.name, '.js') || _endWith(b.name, '.html') || _endWith(b.name, '.css')) { - var data = fs.readFileSync(a).toString().split('\n') - let _license = _endWith(b.name, '.html') ? license_html : license - if (b.name === 'service-worker.js') { - _license = full_license - } - data.splice(0, 0, _license) - var text = data.join("\n") - fs.writeFileSync(a, text) - } -}) - diff --git a/hugegraph-hubble/hubble-fe/config-overrides.js b/hugegraph-hubble/hubble-fe/config-overrides.js deleted file mode 100644 index 0cdde26c7..000000000 --- a/hugegraph-hubble/hubble-fe/config-overrides.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -const { - override, - addLessLoader, - addWebpackAlias, - overrideDevServer, - watchAll, - addWebpackPlugin -} = require('customize-cra'); -const { ProgressPlugin } = require("webpack") -const addProxy = () => (configFunction) => { - configFunction.proxy = { - '/about': { - target: 'http://127.0.0.1:8088', - changeOrigin: true - }, - '/api': { - target: 'http://127.0.0.1:8088', - changeOrigin: true - } - }; - - return configFunction; -}; - -module.exports = { - webpack: override( - addLessLoader({ - javascriptEnabled: true - }), - addWebpackAlias({ - 'hubble-ui': require('path').resolve(__dirname, './src/components/hubble-ui') - }), - addWebpackPlugin(new ProgressPlugin()), - ), - devServer: overrideDevServer(addProxy(), watchAll()) -}; diff --git a/hugegraph-hubble/hubble-fe/package.json b/hugegraph-hubble/hubble-fe/package.json index 711ca918e..5beb432fe 100644 --- a/hugegraph-hubble/hubble-fe/package.json +++ b/hugegraph-hubble/hubble-fe/package.json @@ -1,57 +1,65 @@ { "name": "hubble", - "version": "1.6.0", - "author": "wangzixi", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/hugegraph/hugegraph-hubble" - }, + "version": "0.1.0", + "private": true, "dependencies": { - "@types/classnames": "^2.2.10", - "@types/codemirror": "^0.0.96", - "@types/d3": "^5.7.2", - "@types/file-saver": "^2.0.1", - "@types/jest": "24.0.15", - "@types/lodash-es": "^4.17.3", - "@types/node": "14.11.8", - "@types/react": "16.9.52", - "@types/react-dom": "16.9.8", - "@types/react-highlight-words": "^0.16.1", - "@types/uuid": "^8.3.0", - "@types/validator": "^13.1.0", - "antd": "^4.18.5", - "axios": "^0.19.0", - "classnames": "^2.2.6", - "codemirror": "^5.55.0", - "file-saver": "^2.0.2", - "framer-motion": "^2.1.2", "i18next": "^19.5.3", - "less": "^3.11.3", - "lodash-es": "^4.17.15", - "mobx": "^5.13.0", - "mobx-react": "^6.2.2", - "prettier": "^2.0.5", - "react": "^16.13.1", - "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", - "react-dom": "^16.13.1", - "react-highlight-words": "^0.16.0", "react-i18next": "^11.7.3", - "react-json-view": "^1.19.1", - "react-popper-tooltip": "^2.11.1", - "react-scripts": "3.4.1", - "typescript": "^3.9.6", - "uuid": "^8.3.1", - "validator": "^13.1.1", - "vis-network": "7.3.5", - "wouter": "^2.5.1" + "@types/lodash-es": "^4.17.3", + "@ant-design/icons": "^4.7.0", + "@antv/g6": "^4.6.15", + "@antv/graphin": "^2.7.12", + "@antv/graphin-components": "^2.4.0", + "@antv/graphin-icons": "^1.0.0", + "@antv/x6": "^1.34.6", + "@antv/x6-react-components": "^1.1.20", + "@antv/x6-react-shape": "^1.6.3", + "@babel/eslint-plugin": "^7.18.10", + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^13.5.0", + "3d-force-graph": "^1.71.2", + "ajv": "8.17.1", + "antd": "^4.23.1", + "axios": "^0.27.2", + "codemirror": "^6.0.1", + "cron-expression-validator": "^1.0.20", + "date-fns": "^2.29.3", + "echarts": "^5.4.1", + "http-proxy-middleware": "^2.0.6", + "install": "^0.13.0", + "json-bigint": "^1.0.0", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "node-gyp": "^9.4.0", + "qs": "^6.13.0", + "sass": "^1.54.0", + "react": "18.2.0", + "react-color": "^2.19.3", + "react-dom": "18.2.0", + "react-highlight-words": "^0.18.0", + "react-json-view": "^1.21.3", + "react-router-dom": "^6.3.0", + "react-scripts": "5.0.1", + "screenfull": "^6.0.2", + "typescript": "^4.7.4", + "validator": "^13.7.0", + "vis-network": "^9.1.2", + "web-vitals": "^2.1.4" }, "scripts": { - "start": "react-app-rewired start", - "build": "CI=false && react-app-rewired build && yarn run license", - "test": "react-app-rewired test", - "license": "node add-license.js" + "start": "react-scripts start", + "build": "node scripts/check-i18n.js && react-scripts build", + "lint": "eslint src --ext .js,.jsx,.ts,.tsx --max-warnings=0", + "i18n:check": "node scripts/check-i18n.js", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] }, "browserslist": { "production": [ @@ -66,13 +74,11 @@ ] }, "devDependencies": { - "customize-cra": "^0.9.1", - "less-loader": "^5.0.0", - "react-app-rewired": "^2.1.6", - "stylelint": "^13.6.1", - "stylelint-config-standard": "^20.0.0" - }, - "keywords": [ - "hugegraph" - ] + "@ecomfe/eslint-config": "^7.4.0", + "@ecomfe/stylelint-config": "^1.1.2", + "eslint": "^8.19.0", + "eslint-plugin-react": "^7.30.1", + "playwright": "^1.61.1", + "resize-observer-polyfill": "^1.5.1" + } } diff --git a/hugegraph-hubble/hubble-fe/public/index.html b/hugegraph-hubble/hubble-fe/public/index.html index c5bcd62a8..b41d4bdda 100644 --- a/hugegraph-hubble/hubble-fe/public/index.html +++ b/hugegraph-hubble/hubble-fe/public/index.html @@ -1,38 +1,61 @@ - + - + - - + + - HugeGraph + + HugeGraph Hubble
+ diff --git a/hugegraph-hubble/hubble-fe/public/logo192.png b/hugegraph-hubble/hubble-fe/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/hugegraph-hubble/hubble-fe/public/logo192.png differ diff --git a/hugegraph-hubble/hubble-fe/public/logo512.png b/hugegraph-hubble/hubble-fe/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/hugegraph-hubble/hubble-fe/public/logo512.png differ diff --git a/hugegraph-hubble/hubble-fe/public/manifest.json b/hugegraph-hubble/hubble-fe/public/manifest.json index 1f2f141fa..080d6c77a 100644 --- a/hugegraph-hubble/hubble-fe/public/manifest.json +++ b/hugegraph-hubble/hubble-fe/public/manifest.json @@ -6,6 +6,16 @@ "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" } ], "start_url": ".", diff --git a/hugegraph-hubble/hubble-fe/public/robots.txt b/hugegraph-hubble/hubble-fe/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/hugegraph-hubble/hubble-fe/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/hugegraph-hubble/hubble-fe/scripts/check-i18n.js b/hugegraph-hubble/hubble-fe/scripts/check-i18n.js new file mode 100644 index 000000000..4464caf0f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/scripts/check-i18n.js @@ -0,0 +1,559 @@ +#!/usr/bin/env node + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const fs = require('fs'); +const path = require('path'); + +const projectRoot = path.resolve(__dirname, '..'); +const resourcesRoot = path.join(projectRoot, 'src', 'i18n', 'resources'); +const sourceRoot = path.join(projectRoot, 'src'); +const locales = ['zh-CN', 'en-US']; +const ignoredSourceDirs = new Set(['i18n', 'node_modules']); +const problems = []; +const sharedConstants = loadSharedConstants(); + +if (process.argv.includes('--self-test')) { + runSelfTests(); + process.exit(0); +} + +const localeData = Object.fromEntries( + locales.map((locale) => [locale, loadLocale(locale)]) +); + +checkSymmetricKeys(); +checkLocaleValues(); +checkStaticTranslationUsage(); + +if (problems.length > 0) { + console.error(`i18n check failed with ${problems.length} issue(s):`); + for (const problem of problems) { + console.error(`- ${problem}`); + } + process.exit(1); +} + +console.log(JSON.stringify({ + status: 'passed', + locales: Object.fromEntries( + locales.map((locale) => [locale, Object.keys(localeData[locale].flat).length]) + ), + checkedStaticKeys: collectStaticTranslationKeys().length +}, null, 2)); + +function loadLocale(locale) { + const root = path.join(resourcesRoot, locale); + const files = listFiles(root, (file) => file.endsWith('.json')); + const byFile = new Map(); + const merged = {}; + + for (const file of files) { + const relativePath = path.relative(root, file); + const json = JSON.parse(fs.readFileSync(file, 'utf8')); + byFile.set(relativePath, flatten(json)); + } + + for (const moduleFile of getMergedModuleFiles(locale)) { + const file = path.join(root, moduleFile); + if (fs.existsSync(file)) { + deepMerge(merged, JSON.parse(fs.readFileSync(file, 'utf8'))); + } + } + + return { + byFile, + flat: flatten(Object.fromEntries( + Array.from(byFile.entries()).map(([file, flat]) => [file, flat]) + )), + merged, + mergedFlat: flatten(merged) + }; +} + +function getMergedModuleFiles(locale) { + const indexPath = path.join(resourcesRoot, locale, 'index.js'); + const indexText = fs.readFileSync(indexPath, 'utf8'); + const imports = new Map(); + + for (const barrelImport of getNamedImports(indexText)) { + const barrel = path.join(resourcesRoot, locale, barrelImport.source, 'index.js'); + if (!fs.existsSync(barrel)) { + continue; + } + const barrelText = fs.readFileSync(barrel, 'utf8'); + const barrelImports = getDefaultJsonImports(barrelText, barrelImport.source); + for (const name of barrelImport.names) { + if (barrelImports.has(name)) { + imports.set(name, barrelImports.get(name)); + } + } + } + + for (const [name, file] of getDefaultJsonImports(indexText)) { + imports.set(name, file); + } + + return getMergeArguments(indexText) + .map((name) => imports.get(name)) + .filter(Boolean); +} + +function getDefaultJsonImports(text, parentDir = '') { + const imports = new Map(); + const importPattern = /import\s+(\w+)\s+from\s+(['"])\.\/([^'"]+\.json)\2/g; + let match; + + while ((match = importPattern.exec(text)) !== null) { + imports.set(match[1], path.join(parentDir, match[3])); + } + return imports; +} + +function getNamedImports(text) { + const imports = []; + const importPattern = /import\s+\{([^}]*)\}\s+from\s+(['"])\.\/([^'"]+)\2;?/g; + let match; + + while ((match = importPattern.exec(text)) !== null) { + imports.push({ + names: match[1].split(',').map((name) => name.trim()).filter(Boolean), + source: match[3] + }); + } + return imports; +} + +function getMergeArguments(indexText) { + const start = indexText.indexOf('merge('); + if (start === -1) { + return []; + } + let depth = 0; + let end = start; + for (; end < indexText.length; end++) { + if (indexText[end] === '(') { + depth++; + } else if (indexText[end] === ')') { + depth--; + if (depth === 0) { + break; + } + } + } + return indexText + .slice(start + 'merge('.length, end) + .split(',') + .map((name) => name.trim()) + .filter(Boolean); +} + +function checkSymmetricKeys() { + const zhFiles = localeData['zh-CN'].byFile; + const enFiles = localeData['en-US'].byFile; + const allFiles = new Set([...zhFiles.keys(), ...enFiles.keys()]); + + for (const file of allFiles) { + if (!zhFiles.has(file)) { + problems.push(`zh-CN missing resource file ${file}`); + continue; + } + if (!enFiles.has(file)) { + problems.push(`en-US missing resource file ${file}`); + continue; + } + const zhKeys = new Set(Object.keys(zhFiles.get(file))); + const enKeys = new Set(Object.keys(enFiles.get(file))); + for (const key of zhKeys) { + if (!enKeys.has(key)) { + problems.push(`en-US missing key ${file}:${key}`); + } + } + for (const key of enKeys) { + if (!zhKeys.has(key)) { + problems.push(`zh-CN missing key ${file}:${key}`); + } + } + } +} + +function checkLocaleValues() { + for (const locale of locales) { + for (const [file, entries] of localeData[locale].byFile.entries()) { + for (const [key, value] of Object.entries(entries)) { + if (typeof value !== 'string') { + continue; + } + if (value.trim() === '') { + problems.push(`${locale} empty value ${file}:${key}`); + } + if ( + locale === 'en-US' && + /[\u4e00-\u9fff]/.test(value) + ) { + problems.push(`${locale} Chinese text ${file}:${key}=${value}`); + } + if (locale === 'en-US' && /[,。!?;:()【】《》]/.test(value)) { + problems.push(`${locale} full-width punctuation ${file}:${key}=${value}`); + } + if (/\b(TODO|TBD|xxx)\b/i.test(key) || /\b(TODO|TBD|xxx)\b/i.test(value)) { + problems.push(`${locale} placeholder ${file}:${key}=${value}`); + } + if (looksLikeRawKey(value) && value === key) { + problems.push(`${locale} raw key value ${file}:${key}`); + } + } + } + } +} + +function checkStaticTranslationUsage() { + const staticKeys = collectStaticTranslationKeys(); + const zhMerged = localeData['zh-CN'].mergedFlat; + const enMerged = localeData['en-US'].mergedFlat; + + for (const key of staticKeys) { + if (!Object.prototype.hasOwnProperty.call(zhMerged, key)) { + problems.push(`zh-CN missing merged static t() key ${key}`); + } + if (!Object.prototype.hasOwnProperty.call(enMerged, key)) { + problems.push(`en-US missing merged static t() key ${key}`); + } + } +} + +function collectStaticTranslationKeys() { + const keys = new Set(); + for (const file of listFiles(sourceRoot, isJavaScriptLikeFile)) { + if (shouldIgnoreSource(file)) { + continue; + } + const content = fs.readFileSync(file, 'utf8'); + for (const key of extractStaticTranslationKeys(content, sharedConstants)) { + keys.add(key); + } + } + return Array.from(keys).sort(); +} + +function extractStaticTranslationKeys(content, baseConstants = new Map()) { + const keys = []; + const constants = collectStringConstants(content, baseConstants); + const source = stripComments(content); + const translationPattern = /\bt\(\s*([^)]+)\)/g; + let match; + + while ((match = translationPattern.exec(source)) !== null) { + const key = resolveStringExpression(match[1].trim(), constants); + if (key) { + keys.push(key); + } + } + return keys; +} + +function loadSharedConstants() { + const constants = new Map(); + const constantsPath = path.join(sourceRoot, 'utils', 'constants.js'); + if (!fs.existsSync(constantsPath)) { + return constants; + } + + const content = fs.readFileSync(constantsPath, 'utf8'); + const textPathMatch = content.match(/export\s+const\s+TEXT_PATH\s*=\s*\{([\s\S]*?)\};/); + if (!textPathMatch) { + return constants; + } + + const entryPattern = /(\w+)\s*:\s*(['"])((?:\\[\s\S]|(?!\2)[\s\S])*?)\2/g; + let match; + while ((match = entryPattern.exec(textPathMatch[1])) !== null) { + constants.set(`TEXT_PATH.${match[1]}`, unescapeQuotedString(match[3])); + } + return constants; +} + +function collectStringConstants(content, baseConstants) { + const constants = new Map(baseConstants); + const source = stripComments(content); + const declarationPattern = /\bconst\s+(\w+)\s*=\s*([^;]+);/g; + const declarations = []; + let match; + + while ((match = declarationPattern.exec(source)) !== null) { + declarations.push([match[1], match[2].trim()]); + } + + let changed = true; + while (changed) { + changed = false; + for (const [name, expression] of declarations) { + if (constants.has(name)) { + continue; + } + const value = resolveStringExpression(expression, constants); + if (value) { + constants.set(name, value); + changed = true; + } + } + } + return constants; +} + +function resolveStringExpression(expression, constants) { + const value = expression.trim(); + if (!value) { + return null; + } + + const literal = parseQuotedLiteral(value); + if (literal !== null) { + return literal; + } + + if (constants.has(value)) { + return constants.get(value); + } + + const template = resolveTemplateLiteral(value, constants); + if (template !== null) { + return template; + } + + const parts = splitTopLevelConcat(value); + if (parts.length > 1) { + const resolved = parts.map((part) => resolveStringExpression(part, constants)); + if (resolved.every((part) => part !== null)) { + return resolved.join(''); + } + } + + return null; +} + +function resolveTemplateLiteral(expression, constants) { + if (!expression.startsWith('`') || !expression.endsWith('`')) { + return null; + } + const body = expression.slice(1, -1); + let unresolved = false; + const resolved = body.replace(/\$\{([^}]+)\}/g, (fullMatch, name) => { + const value = resolveStringExpression(name, constants); + if (value === null) { + unresolved = true; + return fullMatch; + } + return value; + }); + return unresolved ? null : unescapeQuotedString(resolved); +} + +function splitTopLevelConcat(expression) { + const parts = []; + let quote = null; + let depth = 0; + let start = 0; + for (let index = 0; index < expression.length; index++) { + const char = expression[index]; + const prev = expression[index - 1]; + if (quote) { + if (char === quote && prev !== '\\') { + quote = null; + } + continue; + } + if (char === '\'' || char === '"' || char === '`') { + quote = char; + continue; + } + if (char === '(') { + depth++; + } else if (char === ')') { + depth--; + } else if (char === '+' && depth === 0) { + parts.push(expression.slice(start, index).trim()); + start = index + 1; + } + } + parts.push(expression.slice(start).trim()); + return parts; +} + +function parseQuotedLiteral(expression) { + if (expression.length < 2) { + return null; + } + const quote = expression[0]; + if ((quote !== '\'' && quote !== '"') || expression[expression.length - 1] !== quote) { + return null; + } + return unescapeQuotedString(expression.slice(1, -1)); +} + +function unescapeQuotedString(value) { + return value.replace(/\\(['"`\\])/g, '$1'); +} + +function stripComments(content) { + let output = ''; + let quote = null; + let lineComment = false; + let blockComment = false; + + for (let index = 0; index < content.length; index++) { + const char = content[index]; + const next = content[index + 1]; + const prev = content[index - 1]; + + if (lineComment) { + if (char === '\n') { + lineComment = false; + output += char; + } else { + output += ' '; + } + continue; + } + + if (blockComment) { + if (char === '*' && next === '/') { + blockComment = false; + output += ' '; + index++; + } else { + output += char === '\n' ? '\n' : ' '; + } + continue; + } + + if (quote) { + output += char; + if (char === quote && prev !== '\\') { + quote = null; + } + continue; + } + + if (char === '\'' || char === '"' || char === '`') { + quote = char; + output += char; + } else if (char === '/' && next === '/') { + lineComment = true; + output += ' '; + index++; + } else if (char === '/' && next === '*') { + blockComment = true; + output += ' '; + index++; + } else { + output += char; + } + } + return output; +} + +function shouldIgnoreSource(file) { + const relative = path.relative(sourceRoot, file); + return relative.split(path.sep).some((segment) => ignoredSourceDirs.has(segment)); +} + +function isJavaScriptLikeFile(file) { + return /\.(js|jsx|ts|tsx)$/.test(file); +} + +function looksLikeRawKey(value) { + return /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)+$/.test(value); +} + +function listFiles(root, predicate) { + const out = []; + for (const entry of fs.readdirSync(root, {withFileTypes: true})) { + const file = path.join(root, entry.name); + if (entry.isDirectory()) { + out.push(...listFiles(file, predicate)); + } else if (predicate(file)) { + out.push(file); + } + } + return out; +} + +function flatten(obj, prefix = '', out = {}) { + for (const [key, value] of Object.entries(obj)) { + const nextKey = prefix ? `${prefix}.${key}` : key; + if (value && typeof value === 'object' && !Array.isArray(value)) { + flatten(value, nextKey, out); + } else { + out[nextKey] = value; + } + } + return out; +} + +function deepMerge(target, source) { + for (const [key, value] of Object.entries(source)) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) { + target[key] = {}; + } + deepMerge(target[key], value); + } else { + target[key] = value; + } + } + return target; +} + +function runSelfTests() { + const defaultImports = getDefaultJsonImports( + 'import common from "./common.json";\nimport home from \'./modules/home.json\';' + ); + assert(defaultImports.get('common') === 'common.json', 'double-quoted JSON import'); + assert(defaultImports.get('home') === path.join('modules', 'home.json'), 'single import'); + + const namedImports = getNamedImports('import {\n common,\n modules\n} from "./modules";'); + assert(namedImports.length === 1, 'multi-line named import'); + assert(namedImports[0].names.join(',') === 'common,modules', 'named import names'); + assert(namedImports[0].source === 'modules', 'double-quoted named import source'); + + const staticKeys = extractStaticTranslationKeys( + 't("addition.save"); // t("missing.comment")\n' + + 't(\'common.cancel\'); t(`dynamic.${id}`); t("quote.\\"key");\n' + + 'const BASE = "analysis.algorithm.olap.item"; t(`${BASE}.PAGE_RANK`);\n' + + 'const OWNED_TEXT_PATH = TEXT_PATH.OLAP + ".page_rank"; t(OWNED_TEXT_PATH + ".desc");', + new Map([ + ['TEXT_PATH.OLAP', 'analysis.algorithm.olap'] + ]) + ); + assert(staticKeys.includes('addition.save'), 'double-quoted static t()'); + assert(staticKeys.includes('common.cancel'), 'single-quoted static t()'); + assert(staticKeys.includes('quote."key'), 'escaped quote in static t()'); + assert(staticKeys.includes('analysis.algorithm.olap.item.PAGE_RANK'), 'template constant t()'); + assert(staticKeys.includes('analysis.algorithm.olap.page_rank.desc'), 'concatenated t()'); + assert(!staticKeys.includes('missing.comment'), 'commented t() ignored'); + assert(!staticKeys.some((key) => key.includes('${')), 'dynamic template key ignored'); + + console.log(JSON.stringify({status: 'passed', selfTests: 13}, null, 2)); +} + +function assert(condition, message) { + if (!condition) { + throw new Error(`self-test failed: ${message}`); + } +} diff --git a/hugegraph-hubble/hubble-fe/scripts/react-app-rewired.js b/hugegraph-hubble/hubble-fe/scripts/react-app-rewired.js new file mode 100644 index 000000000..a71caf9da --- /dev/null +++ b/hugegraph-hubble/hubble-fe/scripts/react-app-rewired.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const { spawnSync } = require('child_process'); + +const script = process.argv[2]; +const extraArgs = process.argv.slice(3); +const nodeMajorVersion = Number(process.versions.node.split('.')[0]); +const env = { ...process.env }; + +if ( + nodeMajorVersion >= 17 && + !hasNodeOption(env.NODE_OPTIONS, '--openssl-legacy-provider') +) { + env.NODE_OPTIONS = appendNodeOption( + env.NODE_OPTIONS, + '--openssl-legacy-provider' + ); +} + +if (script === 'build' && typeof env.CI === 'undefined') { + env.CI = 'false'; +} + +const result = spawnSync( + process.execPath, + [require.resolve('react-app-rewired/bin/index.js'), script, ...extraArgs], + { + env, + stdio: 'inherit' + } +); + +if (result.error) { + throw result.error; +} + +process.exit(result.status === null ? 1 : result.status); + +function appendNodeOption(existingOptions, nextOption) { + return existingOptions ? `${existingOptions} ${nextOption}` : nextOption; +} + +function hasNodeOption(existingOptions, targetOption) { + return typeof existingOptions === 'string' && + existingOptions.split(/\s+/).includes(targetOption); +} diff --git a/hugegraph-hubble/hubble-fe/src/App.css b/hugegraph-hubble/hubble-fe/src/App.css new file mode 100644 index 000000000..4d3da7a78 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/App.css @@ -0,0 +1,92 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.form_attr_add { + border: 1px dashed #40a9ff; + text-align: center; + padding: 4px; + cursor: pointer; +} + +.form_attr_select { + display: inline-block; + width: calc(25% - 12px); + width: 120px; +} + +.form_attr_val { + display: inline-block; + width: 120px; +} + +.form_attr_split { + display: inline-block; + width: 24px; + line-height: 32px; + text-align: center; +} + +.form_attr_border { + border: 1px dashed #2f54eb; + padding: 20px; + margin-bottom: 10px; +} + +.form_attr_button, +.form_attr_table { + margin-bottom: 10px; +} + +.ant-layout-sider-trigger { + border-top: 1px solid rgba(0, 0, 0, .06); +} + +/* .ant-modal-content { + width: 600px; +} */ + +.ant-modal-body { + max-height: 488px; + overflow: auto; +} + +.ant-menu-sub.ant-menu-inline { + background-color: #fff; +} + +.ant-page-header { + padding: 24px 10px 24px; +} + +.ant-page-header-content { + padding-top: 24px; +} + +/* csslint ignore:start */ +.ant-table-thead > tr > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before { + width: 0; +} +/* csslint ignore:end */ + +.ant-table-tbody > tr > td { + border-bottom: 0; +} + +.ant-table-tbody .ant-empty-normal { + margin: 200px 0; +} diff --git a/hugegraph-hubble/hubble-fe/src/App.js b/hugegraph-hubble/hubble-fe/src/App.js new file mode 100644 index 000000000..a879fbdf2 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/App.js @@ -0,0 +1,34 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import Route from './routes'; +import 'antd/dist/antd.css'; +import './App.scss'; +import './App.css'; +import Layout from './layout.ant'; + +function App() { + + return ( +
+ } /> +
+ ); +}; + +export default App; diff --git a/hugegraph-hubble/hubble-fe/src/App.scss b/hugegraph-hubble/hubble-fe/src/App.scss new file mode 100644 index 000000000..e68fdb445 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/App.scss @@ -0,0 +1,44 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +iframe { + border: medium none; +} + +.main { + min-height: calc(100vh - 60px); + + .content { + display: flex; + flex-direction: column; + padding: 10px; + + .container { + background: #fff; + padding-left: 25px; + margin-top: 10px; + + padding: 10px; + flex: 1 auto; + } + } + + .center { + text-align: center; + } +} diff --git a/hugegraph-hubble/hubble-fe/src/App.test.js b/hugegraph-hubble/hubble-fe/src/App.test.js new file mode 100644 index 000000000..5473c0387 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/App.test.js @@ -0,0 +1,32 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {render, screen} from '@testing-library/react'; +import App from './App'; + +jest.mock('./routes', () => ({element}) => ( +
{element}
+)); + +jest.mock('./layout.ant', () => () =>
Hubble layout
); + +test('wires the Hubble layout into the application router', () => { + render(); + expect(screen.getByTestId('app-route')).toBeInTheDocument(); + expect(screen.getByText('Hubble layout')).toBeInTheDocument(); +}); diff --git a/hugegraph-hubble/hubble-fe/src/accessible-click-actions.test.js b/hugegraph-hubble/hubble-fe/src/accessible-click-actions.test.js new file mode 100644 index 000000000..f9a95a7c1 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/accessible-click-actions.test.js @@ -0,0 +1,56 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import fs from 'fs'; +import path from 'path'; + +const roots = [ + 'pages/Graph', 'pages/GraphSpace', 'pages/Schema', 'pages/Datasource', + 'pages/Task', 'pages/TaskEdit', 'pages/TaskDetail', 'pages/Meta', 'pages/My', + 'pages/Account', 'pages/Login', 'pages/Error404', 'modules/analysis', + 'modules/algorithm', 'modules/asyncTasks', 'modules/component', + 'components/ColorSelect', 'components/KeyboardAction', + 'components/FormListAction', 'components/RowActionButton', +]; + +const collectSourceFiles = directory => fs.readdirSync(directory, {withFileTypes: true}) + .flatMap(entry => { + const target = path.join(directory, entry.name); + if (entry.isDirectory()) { + return collectSourceFiles(target); + } + return /\.(js|jsx)$/.test(entry.name) && !/\.test\./.test(entry.name) + ? [target] : []; + }); + +test('reachable workbench actions do not use click-only anchors or containers', () => { + const offenders = roots.flatMap(root => collectSourceFiles(path.join(__dirname, root))) + .filter(file => { + const source = fs.readFileSync(file, 'utf8'); + if (/]*\bonClick=/s.test(source)) { + return true; + } + return [...source.matchAll(/<(?:span|div)\b[^>]*\bonClick=[^>]*>/gs)] + .some(match => !/\bonKeyDown=/.test(match[0]) + || !/\brole=/.test(match[0]) + || !/\btabIndex=/.test(match[0])); + }) + .map(file => path.relative(__dirname, file)); + + expect(offenders).toEqual([]); +}); diff --git a/hugegraph-hubble/hubble-fe/src/api/analysis-algorithm-error.test.js b/hugegraph-hubble/hubble-fe/src/api/analysis-algorithm-error.test.js new file mode 100644 index 000000000..76b014e7f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/analysis-algorithm-error.test.js @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +jest.mock('./request', () => ({ + __esModule: true, + default: { + post: jest.fn(), + }, +})); + +const request = require('./request').default; +const analysis = require('./analysis'); + +describe('algorithm API failure contract', () => { + beforeEach(() => { + request.post.mockReset(); + }); + + it.each([ + ['runOltpInfo', ['DEFAULT', 'g', {algorithmName: 'kneighbor'}]], + ['postOlapInfo', ['DEFAULT', 'g', {algorithm: 'louvain'}]], + ['runOlapVermeer', ['DEFAULT', 'g', {algorithm: 'pagerank'}]], + ])('normalizes rejected %s requests for the shared form contract', async (name, args) => { + request.post.mockRejectedValue({ + response: { + status: 503, + data: {status: 503, message: 'algorithm service unavailable'}, + }, + }); + + await expect(analysis[name](...args)).resolves.toEqual({ + status: 503, + data: null, + message: 'algorithm service unavailable', + }); + }); + + it('preserves successful algorithm responses', async () => { + const success = {status: 200, data: {task_id: 7}, message: 'Success'}; + request.post.mockResolvedValue(success); + + await expect(analysis.postOlapInfo('DEFAULT', 'g', {})).resolves.toBe(success); + }); + + it('preserves resolved business failures', async () => { + const failure = {status: 400, data: null, message: 'invalid source'}; + request.post.mockResolvedValue(failure); + + await expect(analysis.runOltpInfo('DEFAULT', 'g', { + algorithmName: 'kneighbor', + })).resolves.toBe(failure); + }); + + it.each([ + [{data: {status: 401, message: 'expired'}}], + [{response: {status: 401, data: {status: 401, message: 'expired'}}}], + ])('keeps rejected 401 status after request-level redirect handling', async error => { + request.post.mockRejectedValue(error); + + await expect(analysis.postOlapInfo('DEFAULT', 'g', {})).resolves.toEqual({ + status: 401, + data: null, + message: 'expired', + }); + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/api/analysis.js b/hugegraph-hubble/hubble-fe/src/api/analysis.js new file mode 100644 index 000000000..fb0347b30 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/analysis.js @@ -0,0 +1,236 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import request from './request'; +import qs from 'qs'; + +// 图分析通用 +const getGraphSpaceList = () => { + return request.get('/graphspaces/list'); +}; + +const getGraphList = graphSpace => { + return request.get(`/graphspaces/${graphSpace}/graphs/list`); +}; + +const getOlapMode = (graphSpace, graph) => { + return request.get(`/graphspaces/${graphSpace}/graphs/${graph}/graph_read_mode`); +}; + +const switchOlapMode = (graphSpace, graph, params) => { + return request.put(`/graphspaces/${graphSpace}/graphs/${graph}/graph_read_mode`, params); +}; + +const getUploadList = (graphspace, graph) => { + return `/api/v1.3//graphspaces/${graphspace}/graphs/${graph}/import`; +}; + +const getMetaEdgeList = (graphspace, graph) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels`); +}; + +const getMetaVertexList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels`); +}; + +const updateVertexProperties = (graphspace, graph, params) => { + return request.put(`/graphspaces/${graphspace}/graphs/${graph}/vertex/${params.id}`, params); +}; + +const updateEdgeProperties = (graphspace, graph, params) => { + return request.put(`/graphspaces/${graphspace}/graphs/${graph}/edge/${params.id}`, params); +}; + +const getVertexProperties = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/vertexlabel/${params}`); +}; + +const getEdgeProperties = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/edgelabel/${params}`); +}; + +const getExecutionLogs = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/execute-histories`, {params}); +}; + +const getExecutionQuery = (graphspace, graph, params) => { + return request.post( + `/graphspaces/${graphspace}/graphs/${graph}/gremlin-query`, + params, + {suppressBusinessErrorToast: true} + ); +}; + +const getGraphData = (graphspace, graph) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/gremlin-query`); +}; + +const putExecutionQuery = (graphspace, graph, params) => { + return request.put(`/graphspaces/${graphspace}/graphs/${graph}/gremlin-query`, params); +}; + +const getCypherExecutionQuery = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/cypher`, { + params, + suppressBusinessErrorToast: true, + }); +}; + +const getExecutionTask = (graphspace, graph, params) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/gremlin-query/async-task`, params); +}; + +const getCypherTask = (graphspace, graph, params) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/cypher/async-task`, params); +}; + +const fetchManageTaskList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/async-tasks?`, {params}); +}; + +const addFavoriate = (graphspace, graph, params) => { + return request.post(`graphspaces/${graphspace}/graphs/${graph}/gremlin-collections`, params); +}; + +const deleteQueryCollection = (graphspace, graph, id) => { + return request.delete(`graphspaces/${graphspace}/graphs/${graph}/gremlin-collections/${id}`); +}; + +const editQueryCollection = (graphspace, graph, parmas) => { + return request.put(`graphspaces/${graphspace}/graphs/${graph}/gremlin-collections/${parmas.id}`, parmas); +}; + +const fetchFavoriteQueries = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/gremlin-collections?`, {params}); +}; + +const addGraphNode = (graphspace, graph, params) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/vertex`, params); +}; + +const fetchEdgeLabels = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels/${params}`); +}; + +const fetchVertexlinks = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels/${params}/link`); +}; + +const addEdge = (graphspace, graph, params) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/edge`, params); +}; + + +// 图算法 +const runAlgorithmRequest = async requestCall => { + try { + return await requestCall(); + } + catch (error) { + const errorData = error?.response?.data || error?.data; + return { + status: errorData?.status || error?.response?.status || 500, + data: null, + message: errorData?.message || error?.message || '', + }; + } +}; + +const postOlapInfo = (graphspace, graph, data) => { + return runAlgorithmRequest(() => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/algorithms/olap`, data); + }); +}; + +const runOltpInfo = (graphspace, graph, data) => { + return runAlgorithmRequest(() => { + return request.post( + `/graphspaces/${graphspace}/graphs/${graph}/algorithms/oltp/` + data.algorithmName, + data + ); + }); +}; + +const runOlapVermeer = (graphspace, graph, params) => { + return runAlgorithmRequest(() => { + return request.post( + `/graphspaces/${graphspace}/graphs/${graph}/algorithms/vermeer`, + {params} + ); + }); +}; + +// 任务管理 +const fetchAsyncTaskResult = (graphspace, graph, taskId) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/async-tasks/${taskId}`); +}; + +const deleteAsyncTask = (graphspace, graph, selectedTaskIds) => { + const params = qs.stringify(selectedTaskIds, {arrayFormat: 'repeat'}); + return request.delete(`/graphspaces/${graphspace}/graphs/${graph}/async-tasks?` + params); +}; + +const abortAsyncTask = (graphspace, graph, taskId) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/async-tasks/cancel/${taskId}`); +}; + +const getExecuteAsyncTaskList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/execute-histories`, {params}); +}; + +const loadVermeerTask = params => { + return request.post('/vermeer/task', {...params}); +}; + +export { + getGraphSpaceList, + getGraphList, + getOlapMode, + switchOlapMode, + getExecutionLogs, + getExecutionQuery, + getGraphData, + putExecutionQuery, + getExecutionTask, + getCypherTask, + fetchManageTaskList, + addFavoriate, + postOlapInfo, + runOltpInfo, + runOlapVermeer, + fetchAsyncTaskResult, + addGraphNode, + deleteAsyncTask, + abortAsyncTask, + getMetaEdgeList, + getMetaVertexList, + deleteQueryCollection, + editQueryCollection, + fetchFavoriteQueries, + getUploadList, + updateVertexProperties, + updateEdgeProperties, + getCypherExecutionQuery, + getVertexProperties, + getEdgeProperties, + getExecuteAsyncTaskList, + fetchEdgeLabels, + fetchVertexlinks, + addEdge, + loadVermeerTask, +}; diff --git a/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js b/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js new file mode 100644 index 000000000..e4041ab7e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js @@ -0,0 +1,84 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const mockRequest = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), +}; + +jest.mock('./request', () => mockRequest); + +const auth = require('./auth'); + +describe('auth API contract', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('updates the current profile with PUT and a JSON body', () => { + const profile = {nickname: 'Alice', description: 'owner'}; + + auth.updatePersonal(profile); + + expect(mockRequest.put).toHaveBeenCalledWith('/auth/users/personal', profile); + }); + + it.each([ + ['getAllUserList', [{page_no: 1}], 'get', '/auth/users'], + ['getUserInfo', ['user'], 'get', '/auth/users/user'], + ['addUser', [{user_name: 'user'}], 'post', '/auth/users'], + ['updateUser', ['user', {user_nickname: 'User'}], 'put', + '/auth/users/user'], + ['updateAdminspace', ['user', ['SPACE']], 'post', + '/auth/users/updateadminspace/user'], + ['delUser', ['user'], 'delete', '/auth/users/user'], + ])('%s forwards page-owned error controls', (method, args, verb, route) => { + const config = {suppressBusinessErrorToast: true}; + auth[method](...args, config); + + if (verb === 'get' && method === 'getAllUserList') { + expect(mockRequest.get).toHaveBeenCalledWith( + route, {params: args[0], ...config} + ); + return; + } + if (verb === 'delete') { + expect(mockRequest[verb]).toHaveBeenCalledWith( + route, undefined, config + ); + return; + } + if (verb === 'get') { + expect(mockRequest[verb]).toHaveBeenCalledWith(route, config); + return; + } + expect(mockRequest[verb]).toHaveBeenCalledWith( + route, args.at(-1), config + ); + }); + + it('does not expose retired Super or UUAP facades', () => { + expect(auth.getSuperUser).toBeUndefined(); + expect(auth.addSuperUser).toBeUndefined(); + expect(auth.removeSuperUser).toBeUndefined(); + expect(auth.addUuapUser).toBeUndefined(); + expect(auth.getUUapList).toBeUndefined(); + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/api/auth.js b/hugegraph-hubble/hubble-fe/src/api/auth.js new file mode 100644 index 000000000..3987b2042 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/auth.js @@ -0,0 +1,181 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import request from './request'; + +// login +const login = data => { + return request.post('/auth/login', data); +}; + +const logout = () => { + return request.get('/auth/logout'); +}; + +const status = () => { + return request.get('/auth/status', {suppressBusinessErrorToast: true}); +}; + +const getUserList = (params, config = {}) => { + return request.get('/auth/users/list', {...config, params}); +}; + +const getAllUserList = (params, config = {}) => { + return request.get('/auth/users', {...config, params}); +}; + +const getUserInfo = (username, config) => { + return request.get(`/auth/users/${username}`, config); +}; + +const updateUser = (id, data, config) => { + return request.put(`/auth/users/${id}`, data, config); +}; + +const delUser = (id, config) => { + return request.delete(`/auth/users/${id}`, undefined, config); +}; + +const updateAdminspace = (username, data, config) => { + return request.post(`/auth/users/updateadminspace/${username}`, data, config); +}; + +const addUser = (data, config) => { + return request.post('/auth/users', data, config); +}; + +const updatePwd = (username, oldpwd, newpwd) => { + return request.post('/auth/users/updatepwd', {username, oldpwd, newpwd}); +}; + +const importUserUrl = '/api/v1.3/auth/users/batch'; + +export {login, logout, status, getUserList, getAllUserList, getUserInfo, delUser, + updateUser, addUser, updatePwd, importUserUrl, updateAdminspace}; + +// resource + +const getResourceList = (graphspace, params) => { + return request.get(`/graphspaces/${graphspace}/auth/targets`, {params}); +}; + +const addResource = (graphspace, data) => { + return request.post(`/graphspaces/${graphspace}/auth/targets`, data); +}; + +const updateResource = (graphspace, id, data) => { + return request.put(`/graphspaces/${graphspace}/auth/targets/${id}`, data); +}; + +const getResource = (graphspace, id) => { + return request.get(`/graphspaces/${graphspace}/auth/targets/${id}`); +}; + +const delResource = (graphspace, id) => { + return request.delete(`/graphspaces/${graphspace}/auth/targets/${id}`); +}; + +export {getResourceList, addResource, updateResource, getResource, delResource}; + +// role + +const getRoleList = (graphspace, params) => { + return request.get(`/graphspaces/${graphspace}/auth/roles`, {params}); +}; + +const getAllRoleList = graphspace => { + return request.get(`/graphspaces/${graphspace}/auth/roles/list`); +}; + +const addRole = (graphspace, data) => { + return request.post(`/graphspaces/${graphspace}/auth/roles`, data); +}; + +const updateRole = (graphspace, id, data) => { + return request.put(`/graphspaces/${graphspace}/auth/roles/${id}`, data); +}; + +const delRole = (graphspace, id) => { + return request.delete(`/graphspaces/${graphspace}/auth/roles/${id}`); +}; + +const delRoleBatch = (graphspace, data) => { + return request.delete(`/graphspaces/${graphspace}/auth/roles/`, data); +}; + +const getRoleResourceList = (graphspace, params) => { + return request.get(`/graphspaces/${graphspace}/auth/accesses`, {params}); +}; + +const addRoleResource = (graphspace, data) => { + return request.post(`/graphspaces/${graphspace}/auth/accesses`, data); +}; + +const updateRoleResource = (graphspace, data) => { + return request.put(`/graphspaces/${graphspace}/auth/accesses`, data); +}; + +const delRoleResource = (graphspace, data) => { + return request.delete(`/graphspaces/${graphspace}/auth/accesses`, data); +}; + +const getRoleUser = (graphspace, params) => { + return request.get(`/graphspaces/${graphspace}/auth/belongs`, {params}); +}; + +const addRoleUser = (graphspace, data) => { + return request.post(`/graphspaces/${graphspace}/auth/belongs`, data); +}; + +const addRoleUserBatch = (graphspace, data) => { + return request.post(`/graphspaces/${graphspace}/auth/belongs/ids`, data); +}; + +const delRoleUser = (graphspace, id) => { + return request.delete(`/graphspaces/${graphspace}/auth/belongs/${id}`); +}; + +const delRoleUserBatch = (graphspace, data) => { + return request.post(`/graphspaces/${graphspace}/auth/belongs/delids`, data); +}; + +export { + getRoleList, getAllRoleList, addRole, updateRole, delRole, delRoleBatch, + getRoleResourceList, addRoleResource, updateRoleResource, delRoleResource, + getRoleUser, addRoleUser, addRoleUserBatch, delRoleUser, delRoleUserBatch, +}; + +const getPersonal = config => { + return request.get('/auth/users/getpersonal', config); +}; + +const updatePersonal = data => { + return request.put('/auth/users/personal', data); +}; + +export {getPersonal, updatePersonal}; + +const getDashboard = () => { + return request.get('/dashboard'); +}; + +const getVermeer = () => { + return request.get('/vermeer'); +}; + +export {getDashboard, getVermeer}; diff --git a/hugegraph-hubble/hubble-fe/src/api/cloud.js b/hugegraph-hubble/hubble-fe/src/api/cloud.js new file mode 100644 index 000000000..5d9e14a54 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/cloud.js @@ -0,0 +1,53 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +// import request from './request'; +// const getAccountsList = username => { +// return request.get('/uic/accounts', {params: {username}}); +// }; +// +// const getOrderList = params => { +// return request.get('/order', {params}); +// }; +// +// const getOrder = id => { +// return request.get(`/order/${id}`); +// }; +// +// const updateOrder = (id, data) => { +// return request.put(`/order/${id}`, data); +// }; +// +// const updateOrderStatus = (id, status) => { +// return request.put(`/order/${id}/status`, {status}); +// }; +// +// const createOrder = data => { +// return request.post('/order', data); +// // return request.post1('/settle', qs.stringify(data), +// // {headers: {'Content-Type': 'application/x-www-form-urlencoded'}}); +// }; +// +// const getBillList = params => { +// return request.get('/bill', {params}); +// }; +// +// export { +// getAccountsList, createOrder, getOrderList, getOrder, updateOrder, updateOrderStatus, +// getBillList, +// }; diff --git a/hugegraph-hubble/hubble-fe/src/api/config.js b/hugegraph-hubble/hubble-fe/src/api/config.js new file mode 100644 index 000000000..f4130000c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/config.js @@ -0,0 +1,25 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import request from './request'; + +const getConfig = () => { + return request.get('/config'); +}; + +export {getConfig}; diff --git a/hugegraph-hubble/hubble-fe/src/api/index.js b/hugegraph-hubble/hubble-fe/src/api/index.js new file mode 100644 index 000000000..61311a0fc --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/index.js @@ -0,0 +1,28 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import request from './request'; +import * as manage from './manage'; +import * as auth from './auth'; +import * as analysis from './analysis'; +import * as cloud from './cloud'; +import * as config from './config'; + +const uploadUrl = '/api/v1.3/ingest/files/upload'; + +export {request, manage, auth, analysis, cloud, config, uploadUrl}; diff --git a/hugegraph-hubble/hubble-fe/src/api/languageHeader.js b/hugegraph-hubble/hubble-fe/src/api/languageHeader.js new file mode 100644 index 000000000..8aa429c7f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/languageHeader.js @@ -0,0 +1,39 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const DEFAULT_LANGUAGE = 'zh-CN'; +const ACCEPT_LANGUAGE = 'Accept-Language'; + +export const getCurrentLanguage = () => { + return localStorage.getItem('languageType') || DEFAULT_LANGUAGE; +}; + +export const withLanguageHeader = (headers = {}) => { + const hasLanguageHeader = Object.keys(headers).some( + key => key.toLowerCase() === ACCEPT_LANGUAGE.toLowerCase() + ); + + if (hasLanguageHeader) { + return {...headers}; + } + + return { + ...headers, + [ACCEPT_LANGUAGE]: getCurrentLanguage(), + }; +}; diff --git a/hugegraph-hubble/hubble-fe/src/api/manage-contract.test.js b/hugegraph-hubble/hubble-fe/src/api/manage-contract.test.js new file mode 100644 index 000000000..70c4a33a0 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/manage-contract.test.js @@ -0,0 +1,148 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import request from './request'; +import * as manage from './manage'; + +jest.mock('./request', () => ({ + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +test('updates a graph with PUT JSON on the canonical route', () => { + manage.updateGraph('DEFAULT', 'g', {nickname: 'nick'}); + expect(request.put).toHaveBeenCalledWith( + '/graphspaces/DEFAULT/graphs/g', + {nickname: 'nick'} + ); +}); + +test('clears graph data with POST on the canonical route', () => { + manage.clearGraphData('DEFAULT', 'g'); + + expect(request.post).toHaveBeenCalledWith( + '/graphspaces/DEFAULT/graphs/g/clear' + ); + expect(request.get).not.toHaveBeenCalled(); + expect(manage.clearGraphDataAndSchema).toBeUndefined(); +}); + +test('reads the default graph from the canonical route', () => { + manage.getDefaultGraph('DEFAULT'); + expect(request.get).toHaveBeenCalledWith( + 'graphspaces/DEFAULT/graphs/default' + ); +}); + +test('forwards default-graph error ownership controls', () => { + const config = {suppressBusinessErrorToast: true}; + + manage.getDefaultGraph('DEFAULT', config); + expect(request.get).toHaveBeenCalledWith( + 'graphspaces/DEFAULT/graphs/default', + config + ); + + manage.setDefaultGraph('DEFAULT', 'g', config); + expect(request.post).toHaveBeenCalledWith( + 'graphspaces/DEFAULT/graphs/g/default', + undefined, + config + ); +}); + +test('keeps graph-list request controls out of query parameters', () => { + manage.getGraphList( + 'DEFAULT', + {page_no: 1, page_size: 10}, + {suppressBusinessErrorToast: true} + ); + + expect(request.get).toHaveBeenCalledWith( + '/graphspaces/DEFAULT/graphs', + { + params: {page_no: 1, page_size: 10}, + suppressBusinessErrorToast: true, + } + ); +}); + +test('does not expose default GraphSpace mutation facades', () => { + expect(manage.setDefaultGraphSpace).toBeUndefined(); + expect(manage.getDefaultGraphSpace).toBeUndefined(); +}); + +test.each([ + ['checkMetaProperty', 'post', + '/graphspaces/DEFAULT/graphs/g/schema/propertykeys/check_using'], + ['checkMetaVertex', 'post', + '/graphspaces/DEFAULT/graphs/g/schema/vertexlabels/check_using'], +])('%s forwards request controls outside the request body', (method, verb, route) => { + manage[method]('DEFAULT', 'g', {names: ['name']}, { + suppressBusinessErrorToast: true, + }); + + expect(request[verb]).toHaveBeenCalledWith( + route, + {names: ['name']}, + {suppressBusinessErrorToast: true} + ); +}); + +test.each([ + ['addGraphSpace', [{name: 'SPACE'}], 'post', '/graphspaces'], + ['updateGraphSpace', ['SPACE', {nickname: 'Space'}], 'put', '/graphspaces/SPACE'], + ['delGraphSpace', ['SPACE'], 'delete', '/graphspaces/SPACE'], + ['initBuiltin', [{init_space: true}], 'post', '/graphspaces/builtin'], + ['addSchema', ['SPACE', {name: 'schema'}], 'post', + '/graphspaces/SPACE/schematemplates'], + ['updateSchema', ['SPACE', 'schema', {schema: 'g'}], 'put', + 'graphspaces/SPACE/schematemplates/schema'], + ['delSchema', ['SPACE', 'schema'], 'delete', + 'graphspaces/SPACE/schematemplates/schema'], +])('%s forwards page-owned error controls', (method, args, verb, route) => { + const config = {suppressBusinessErrorToast: true}; + manage[method](...args, config); + + const expected = verb === 'delete' + ? [route, undefined, config] + : [route, args.at(-1), config]; + expect(request[verb]).toHaveBeenCalledWith(...expected); +}); + +test.each([ + ['delMetaProperty', '/schema/propertykeys'], + ['delMetaVertex', '/schema/vertexlabels'], + ['delMetaEdge', '/schema/edgelabels'], +])('%s forwards request controls to the delete request', (method, suffix) => { + manage[method]('DEFAULT', 'g', {names: ['name']}, { + suppressBusinessErrorToast: true, + }); + + expect(request.delete).toHaveBeenCalledWith( + `/graphspaces/DEFAULT/graphs/g${suffix}?names=name&skip_using=false`, + undefined, + {suppressBusinessErrorToast: true} + ); +}); diff --git a/hugegraph-hubble/hubble-fe/src/api/manage.js b/hugegraph-hubble/hubble-fe/src/api/manage.js new file mode 100644 index 000000000..863c97480 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/manage.js @@ -0,0 +1,356 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import request from './request'; + +// 图空间 +const getGraphSpaceList = (params, config = {}) => { + return request.get('/graphspaces', {...config, params}); +}; + +const getGraphSpace = (graphspace, config) => { + return request.get(`/graphspaces/${graphspace}`, config); +}; + +const addGraphSpace = (data, config) => { + return request.post('/graphspaces', data, config); +}; + +const updateGraphSpace = (graphspace, data, config) => { + return request.put(`/graphspaces/${graphspace}`, data, config); +}; + +const delGraphSpace = (graphspace, config) => { + return request.delete(`/graphspaces/${graphspace}`, undefined, config); +}; + +const initBuiltin = (params, config) => { + return request.post('/graphspaces/builtin', params, config); +}; + +export {getGraphSpace, getGraphSpaceList, addGraphSpace, updateGraphSpace, + delGraphSpace, initBuiltin}; + +// schema +const getSchemaList = (graphspace, params, config = {}) => { + return request.get(`/graphspaces/${graphspace}/schematemplates`, {...config, params}); +}; + +const addSchema = (graphspace, data, config) => { + return request.post(`/graphspaces/${graphspace}/schematemplates`, data, config); +}; + +const getSchema = (graphspace, name) => { + return request.get(`graphspaces/${graphspace}/schematemplates/${name}`); +}; + +const getGraphSchema = (graphspace, graph) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/groovy`); +}; + +const exportSchema = (graphspace, graph) => { + return request.get(`graphspaces/${graphspace}/graphs/${graph}/schema/groovy/export`); +}; + +const updateSchema = (graphspace, name, data, config) => { + return request.put(`graphspaces/${graphspace}/schematemplates/${name}`, data, config); +}; + +const delSchema = (graphspace, name, config) => { + return request.delete(`graphspaces/${graphspace}/schematemplates/${name}`, undefined, config); +}; + +export {getSchemaList, addSchema, updateSchema, getSchema, getGraphSchema, exportSchema, delSchema}; + +// 图 +const getGraphList = (graphspace, params, config = {}) => { + return request.get(`/graphspaces/${graphspace}/graphs`, {...config, params}); +}; + +const addGraph = (graphspace, data) => { + const {graph, nickname, schema} = data; + return request.post(`/graphspaces/${graphspace}/graphs/${graph}`, { + nickname, + schema, + }); +}; + +const updateGraph = (graphspace, graph, params) => { + return request.put(`/graphspaces/${graphspace}/graphs/${graph}`, params); +}; + +const getGraph = (graphspace, graph, config) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/get`, config); +}; + +const delGraph = (graphspace, graph) => { + return request.delete(`/graphspaces/${graphspace}/graphs/${graph}`); +}; + +const getGraphView = (graphspace, graph) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/graphview`); +}; + +const setDefaultGraph = (graphspace, graph, config) => { + const path = `graphspaces/${graphspace}/graphs/${graph}/default`; + return config ? request.post(path, undefined, config) : request.post(path); +}; + +const getDefaultGraph = (graphspace, config) => { + const path = `graphspaces/${graphspace}/graphs/default`; + return config ? request.get(path, config) : request.get(path); +}; + +const clearGraphData = (graphspace, graph) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/clear`); +}; + +const getGraphStatistic = (graphspace, graph, config) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/statistics`, config); +}; + +const updateGraphStatistic = (graphspace, graph) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/statistics`); +}; + +const cloneGraph = (graphspace, graph, params) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/clone`, params); +}; + +export {getGraphList, getGraph, addGraph, updateGraph, delGraph, getDefaultGraph, + getGraphView, setDefaultGraph, clearGraphData, + getGraphStatistic, updateGraphStatistic, cloneGraph}; + +// meta property +const getMetaPropertyList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/propertykeys`, {params}); +}; + +const addMetaProperty = (graphspace, graph, data) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/schema/propertykeys`, data); +}; + +const checkMetaProperty = (graphspace, graph, data, config) => { + return request.post( + `/graphspaces/${graphspace}/graphs/${graph}/schema/propertykeys/check_using`, data, config); +}; + +const updateMetaProperty = () => {}; + +const delMetaProperty = (graphspace, graph, data, config) => { + const {names} = data; + const str = names.map(name => 'names=' + encodeURIComponent(name)).join('&'); + const skip_using = String(names.length !== 1); + + // return request.delete(`/graphspaces/${graphspace}/graphs/${graph}/schema/propertykeys`, data); + return request.delete( + `/graphspaces/${graphspace}/graphs/${graph}/schema/propertykeys?${str}&skip_using=${skip_using}`, + undefined, config); +}; + +export {getMetaPropertyList, addMetaProperty, updateMetaProperty, delMetaProperty, checkMetaProperty}; + +// meta vertex +const getMetaVertexList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels`, {params}); +}; + +const getMetaVertex = (graphspace, graph, name) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels/${name}`); +}; + +const getMetaVertexLink = (graphspace, graph, name) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels/${name}/link`); +}; + +const getMetaVertexNew = (graphspace, graph, name) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels/${name}/new`); +}; + +const addMetaVertex = (graphspace, graph, data) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels`, data); +}; + +const addMetaVertexNew = (graphspace, graph, data) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels/create_new`, data); +}; + +const checkMetaVertex = (graphspace, graph, data, config) => { + return request.post( + `/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels/check_using`, data, config); +}; + +const updateMetaVertex = (graphspace, graph, name, data) => { + return request.put(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels/${name}`, data); +}; + +const delMetaVertex = (graphspace, graph, data, config) => { + // return request.delete(`/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels`, data); + + const {names} = data; + const str = names.map(name => 'names=' + encodeURIComponent(name)).join('&'); + const skip_using = String(names.length !== 1); + + return request.delete( + `/graphspaces/${graphspace}/graphs/${graph}/schema/vertexlabels?${str}&skip_using=${skip_using}`, + undefined, config); +}; + +export {getMetaVertexList, addMetaVertex, updateMetaVertex, delMetaVertex, checkMetaVertex, getMetaVertex, + getMetaVertexNew, addMetaVertexNew, getMetaVertexLink}; + +// meta edge +const getMetaEdgeList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels`, {params}); +}; + +const getMetaEdge = (graphspace, graph, name) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels/${name}`); +}; + +const addMetaEdge = (graphspace, graph, data) => { + return request.post(`/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels`, data); +}; + +const updateMetaEdge = (graphspace, graph, name, data) => { + return request.put(`/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels/${name}`, data); +}; + +const delMetaEdge = (graphspace, graph, data, config) => { + // return request.delete(`/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels`, data); + + const {names} = data; + const str = names.map(name => 'names=' + encodeURIComponent(name)).join('&'); + const skip_using = String(names.length !== 1); + + return request.delete( + `/graphspaces/${graphspace}/graphs/${graph}/schema/edgelabels?${str}&skip_using=${skip_using}`, + undefined, config); +}; + +export {getMetaEdgeList, getMetaEdge, addMetaEdge, updateMetaEdge, delMetaEdge}; + +// meta vertex index +const getMetaVertexIndexList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/propertyindexes?is_vertex_label=true`, + {params}); +}; + +// meta edge index +const getMetaEdgeIndexList = (graphspace, graph, params) => { + return request.get(`/graphspaces/${graphspace}/graphs/${graph}/schema/propertyindexes?is_vertex_label=false`, + {params}); +}; + +export {getMetaVertexIndexList, getMetaEdgeIndexList}; + +// datasource +const testhost = '/ingest'; +const getDatasourceList = params => { + // return request.get('/datasources/list', data); + return request.get(`${testhost}/datasources/list`, {params}); +}; + +const getDatasource = id => { + // return request.get(`/datasources/${id}`); + return request.get(`${testhost}/datasources/${id}`); +}; + +const addDatasource = data => { + // return request.post('/datasources', data); + return request.post(`${testhost}/datasources`, data); +}; + +// const updateDatasource = () => {}; + +const delDatasource = id => { + // return request.delete(`/datasources/${id}`); + return request.delete(`${testhost}/datasources/${id}`); +}; + +const delBatchDatasource = data => { + return request.post(`${testhost}/datasources/delete`, data); +}; + +const getDatasourceSchema = datasourceID => { + return request.get(`${testhost}/schemas`, {params: {datasource: datasourceID}}); +}; + +const checkJDBC = data => { + return request.post(`${testhost}/jdbc/check`, data); +}; + + +const datasourceUploadUrl = '/api/v1.3/ingest/files/upload'; + +export {getDatasource, getDatasourceList, addDatasource, delDatasource, + getDatasourceSchema, delBatchDatasource, checkJDBC, datasourceUploadUrl}; + +// task +const addTask = data => { + return request.post(`${testhost}/tasks`, data, { + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + }); +}; + +const getTaskList = params => { + return request.get(`${testhost}/tasks/list`, {params}); +}; + +const getTaskDetail = id => { + return request.get(`${testhost}/tasks/${id}`); +}; + +const deleteTask = id => { + return request.delete(`${testhost}/tasks/${id}`); +}; + +const disableTask = id => { + return request.put(`${testhost}/tasks/${id}/disable`); +}; + +const enableTask = id => { + return request.put(`${testhost}/tasks/${id}/enable`); +}; + +const updateTask = (id, data) => { + return request.put(`${testhost}/tasks/${id}`, data); +}; + +const getMetricsTask = () => { + return request.get(`${testhost}/metrics/task`); +}; + +export {addTask, getTaskList, getTaskDetail, deleteTask, disableTask, enableTask, updateTask, getMetricsTask}; + +// job +const getJobsList = params => { + return request.get(`${testhost}/jobs/list`, {params}); +}; + +const getJobsDetail = id => { + return request.get(`${testhost}/jobs/${id}`); +}; + +const deleteJobs = id => { + return request.delete(`${testhost}/jobs/${id}`); +}; + +export {getJobsList, getJobsDetail, deleteJobs}; diff --git a/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js new file mode 100644 index 000000000..761f23ce0 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js @@ -0,0 +1,229 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const loadResponseHandlers = modulePath => { + jest.resetModules(); + + const responseHandlers = []; + const messageError = jest.fn(); + const modalWarning = jest.fn(); + const clearLogin = jest.fn(); + const instance = { + interceptors: { + request: { + use: jest.fn(), + }, + response: { + use: jest.fn((resolve, reject) => { + responseHandlers.push({resolve, reject}); + }), + }, + }, + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + }; + + jest.doMock('axios', () => ({ + create: jest.fn(() => instance), + })); + jest.doMock('antd', () => ({ + message: { + error: messageError, + }, + Modal: { + warning: modalWarning, + }, + })); + jest.doMock('../i18n', () => ({ + t: key => key, + })); + jest.doMock('../utils/user', () => ({ + clearLogin, + })); + + const request = require(modulePath).default; + return { + resolve: responseHandlers[0].resolve, + reject: responseHandlers[0].reject, + messageError, + modalWarning, + clearLogin, + instance, + request, + }; +}; + +describe.each(['./request'])('%s error semantics', modulePath => { + beforeEach(() => { + delete window.location; + window.location = { + pathname: '/navigation', + search: '?from=test', + href: '', + }; + }); + + afterEach(() => { + jest.dontMock('axios'); + jest.dontMock('antd'); + jest.dontMock('../i18n'); + jest.dontMock('../utils/user'); + localStorage.clear(); + sessionStorage.clear(); + }); + + it('keeps non-401 HTTP errors rejected after showing the error message', async () => { + const {reject, messageError} = loadResponseHandlers(modulePath); + const error = { + response: { + status: 500, + data: { + message: 'boom', + path: '/api/v1.3/graphs', + }, + }, + }; + + await expect(reject(error)).rejects.toBe(error); + expect(messageError).toHaveBeenCalledWith('request.error'); + }); + + it('keeps network errors rejected after showing the fallback message', async () => { + const {reject, messageError} = loadResponseHandlers(modulePath); + const error = new Error('Network Error'); + + await expect(reject(error)).rejects.toBe(error); + expect(messageError).toHaveBeenCalledWith('request.error'); + }); + + it('rejects HTTP 401 and redirects to login', async () => { + const {reject, clearLogin} = loadResponseHandlers(modulePath); + const error = { + response: { + status: 401, + data: { + status: 401, + message: 'Unauthorized', + }, + }, + }; + + await expect(reject(error)).rejects.toBe(error); + expect(clearLogin).toHaveBeenCalledTimes(1); + expect(sessionStorage.getItem('redirect')).toBe('/navigation?from=test'); + expect(window.location.href).toBe('/login?redirect=%2Fnavigation%3Ffrom%3Dtest'); + }); + + it('rejects business 401 and redirects to login', async () => { + const {resolve, clearLogin} = loadResponseHandlers(modulePath); + const response = { + status: 200, + data: { + status: 401, + message: 'Unauthorized', + }, + }; + + await expect(resolve(response)).rejects.toBe(response); + expect(clearLogin).toHaveBeenCalledTimes(1); + expect(window.location.href).toBe('/login?redirect=%2Fnavigation%3Ffrom%3Dtest'); + }); + + it('shows a modal warning for throttled login attempts', () => { + const {resolve, modalWarning, messageError} = loadResponseHandlers(modulePath); + const response = { + status: 200, + data: { + status: 429, + message: 'Retry in 2 seconds', + }, + }; + + expect(resolve(response)).toBe(response); + expect(modalWarning).toHaveBeenCalledWith(expect.objectContaining({ + content: 'Retry in 2 seconds', + })); + expect(messageError).not.toHaveBeenCalled(); + }); + + it('shows only one warning while repeated HTTP throttles are active', async () => { + const {reject, modalWarning, messageError} = loadResponseHandlers(modulePath); + const error = { + response: { + status: 429, + data: { + status: 429, + message: 'Retry in 5 seconds', + }, + }, + }; + + await expect(reject(error)).rejects.toBe(error); + await expect(reject(error)).rejects.toBe(error); + expect(modalWarning).toHaveBeenCalledTimes(1); + expect(messageError).not.toHaveBeenCalled(); + }); + + it('lets an inline error owner suppress the duplicate business-error toast', () => { + const {resolve, messageError} = loadResponseHandlers(modulePath); + const response = { + status: 200, + config: {suppressBusinessErrorToast: true}, + data: { + status: 400, + message: 'internal implementation details', + }, + }; + + expect(resolve(response)).toBe(response); + expect(messageError).not.toHaveBeenCalled(); + }); + + it('lets an inline error owner suppress the duplicate transport-error toast', async () => { + const {reject, messageError} = loadResponseHandlers(modulePath); + const error = { + config: {suppressBusinessErrorToast: true}, + response: { + status: 500, + data: {message: 'connection refused'}, + }, + }; + + await expect(reject(error)).rejects.toBe(error); + expect(messageError).not.toHaveBeenCalled(); + }); + + it('forwards page-owned error controls through PUT and DELETE', async () => { + const {instance, request} = loadResponseHandlers(modulePath); + const config = {suppressBusinessErrorToast: true}; + instance.put.mockResolvedValue({data: {status: 200}}); + instance.delete.mockResolvedValue({data: {status: 200}}); + + await request.put('/resource', {name: 'value'}, config); + await request.delete('/resource', {name: 'value'}, config); + + expect(instance.put).toHaveBeenCalledWith( + '/resource', {name: 'value'}, config + ); + expect(instance.delete).toHaveBeenCalledWith( + '/resource', {params: {name: 'value'}, ...config} + ); + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/api/request-language-header.test.js b/hugegraph-hubble/hubble-fe/src/api/request-language-header.test.js new file mode 100644 index 000000000..d69c20025 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/request-language-header.test.js @@ -0,0 +1,88 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const loadRequestInterceptor = modulePath => { + jest.resetModules(); + + const requestHandlers = []; + const instance = { + interceptors: { + request: { + use: jest.fn(handler => requestHandlers.push(handler)), + }, + response: { + use: jest.fn(), + }, + }, + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + }; + + jest.doMock('axios', () => ({ + create: jest.fn(() => instance), + })); + jest.doMock('antd', () => ({ + message: { + error: jest.fn(), + }, + })); + jest.doMock('../i18n', () => ({ + t: key => key, + })); + + require(modulePath); + return requestHandlers[0]; +}; + +describe('request language header', () => { + afterEach(() => { + jest.dontMock('axios'); + jest.dontMock('antd'); + jest.dontMock('../i18n'); + localStorage.clear(); + }); + + it('adds the selected language to JSON requests', () => { + localStorage.setItem('languageType', 'en-US'); + const intercept = loadRequestInterceptor('./request'); + + const config = intercept({ + headers: {}, + data: {gremlin: 'bad('}, + }); + + expect(config.headers['Accept-Language']).toBe('en-US'); + }); + + it('preserves existing headers while adding the selected language to form requests', () => { + localStorage.setItem('languageType', 'zh-CN'); + const intercept = loadRequestInterceptor('./request'); + + const config = intercept({ + headers: { + 'X-Test': 'keep', + }, + data: {name: 'hugegraph'}, + }); + + expect(config.headers['Accept-Language']).toBe('zh-CN'); + expect(config.headers['X-Test']).toBe('keep'); + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/api/request.js b/hugegraph-hubble/hubble-fe/src/api/request.js new file mode 100644 index 000000000..0b2aacaab --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/request.js @@ -0,0 +1,168 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import axios from 'axios'; +import {message} from 'antd'; +import JSONbig from 'json-bigint'; +import _ from 'lodash'; +import i18n from '../i18n'; +import * as user from '../utils/user'; +import {withLanguageHeader} from './languageHeader'; +import {showThrottleWarning} from './throttleWarning'; + +const isJsonResponse = headers => { + const contentType = headers?.['content-type'] || headers?.['Content-Type'] || ''; + return contentType.includes('application/json'); +}; + +const parseResponse = (data, headers) => { + if (!data || !isJsonResponse(headers)) { + return data; + } + return JSONbig.parse(data); +}; + +const redirectToLogin = () => { + user.clearLogin(); + if (window.location.pathname !== '/login') { + const redirect = `${window.location.pathname}${window.location.search}`; + sessionStorage.setItem('redirect', redirect); + window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`; + } +}; + +const showRequestError = res => { + message.error(i18n.t('request.error', { + message: res?.message ?? '', + path: res?.path ?? '', + })); +}; + +const isUnauthorizedError = error => { + return error.response?.status === 401 + || error.response?.data?.status === 401 + || error.message?.includes('status code 401'); +}; + +const instance = axios.create({ + baseURL: '/api/v1.3', + withCredentials: true, + // Backend times out after 30s; keep this slightly higher to receive its error body. + timeout: 31000, + transformResponse: [parseResponse], +}); + +instance.interceptors.request.use( + config => { + config.headers = withLanguageHeader(config.headers); + if (!config.headers['Content-Type']) { + if (config.data !== undefined) { + config.data = JSON.stringify(config.data); + } + config.headers = { + ...config.headers, + 'Content-Type': 'application/json;charset=UTF-8', + }; + } + return config; + }, + error => { + return Promise.reject(error); + } +); + +instance.interceptors.response.use( + response => { + if (response.status === 401 || response.data?.status === 401) { + redirectToLogin(); + return Promise.reject(response); + } + else if (response.data?.status === 429) { + showThrottleWarning(response.data.message); + } + else if (response.data?.status !== 200 + && !response.config?.suppressBusinessErrorToast) { + if (!_.isEmpty(response.data.message)) { + message.error(response.data.message); + } + } + return response; + }, + error => { + if (isUnauthorizedError(error)) { + redirectToLogin(); + return Promise.reject(error); + } + if (error.response?.status === 429 + || error.response?.data?.status === 429) { + showThrottleWarning(error.response?.data?.message); + return Promise.reject(error); + } + if (!error.config?.suppressBusinessErrorToast) { + const res = error.response?.data; + showRequestError(res); + } + return Promise.reject(error); + } +); + +const request = {}; + +const responseData = response => { + const data = response?.data; + if (data?.status === 401) { + redirectToLogin(); + } + return data; +}; + +request.get = async (url, params) => { + const resposne = await instance.get(`${url}`, params); + return responseData(resposne); +}; + +request.post = async (url, params, config) => { + const resposne = await instance.post( + `${url}`, + params, + config + ); + + return responseData(resposne); +}; + +request.put = async (url, params, config) => { + const resposne = await instance.put( + `${url}`, + params, + config + ); + + return responseData(resposne); +}; + +request.delete = async (url, params, config) => { + const resposne = await instance.delete( + `${url}`, + {...config, params} + ); + + return responseData(resposne); +}; + +export default request; diff --git a/hugegraph-hubble/hubble-fe/src/api/throttleWarning.js b/hugegraph-hubble/hubble-fe/src/api/throttleWarning.js new file mode 100644 index 000000000..1f1718f73 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/api/throttleWarning.js @@ -0,0 +1,32 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {Modal} from 'antd'; + +let warningOpen = false; + +export const showThrottleWarning = content => { + if (warningOpen) { + return; + } + warningOpen = true; + const close = () => { + warningOpen = false; + }; + Modal.warning({content, onOk: close, afterClose: close}); +}; diff --git a/hugegraph-hubble/hubble-fe/src/assets/canvas_bg.png b/hugegraph-hubble/hubble-fe/src/assets/canvas_bg.png new file mode 100644 index 000000000..1fef89309 Binary files /dev/null and b/hugegraph-hubble/hubble-fe/src/assets/canvas_bg.png differ diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow.svg b/hugegraph-hubble/hubble-fe/src/assets/ic_arrow.svg similarity index 99% rename from hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow.svg rename to hugegraph-hubble/hubble-fe/src/assets/ic_arrow.svg index 1885992f9..e4cd7be86 100644 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow.svg +++ b/hugegraph-hubble/hubble-fe/src/assets/ic_arrow.svg @@ -1,5 +1,6 @@ - - - - ic_add@2x - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_add_node.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_add_node.svg deleted file mode 100644 index 66347e7aa..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_add_node.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - 新增 - Created with Sketch. - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_16.svg deleted file mode 100644 index 4c6894819..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_16.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - ic_arrow_16 - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_blue.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_blue.svg deleted file mode 100644 index f58bcebbf..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_blue.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - 编组 5 - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_white.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_white.svg deleted file mode 100644 index ed96b68d4..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_arrow_white.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Rectangle 2 - Created with Sketch. - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32.svg deleted file mode 100644 index c5744cf0f..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Group 2 - Created with Sketch. - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32_normal.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32_normal.svg deleted file mode 100644 index 3bdd14066..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32_normal.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - ic_back_32_normal - Created with Sketch. - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32_pressed.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32_pressed.svg deleted file mode 100644 index d76a624ec..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_back_32_pressed.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - ic_back_32_pressed - Created with Sketch. - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_biaoge_hover.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_biaoge_hover.svg deleted file mode 100644 index dbc401bc6..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_biaoge_hover.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - ic_biaoge_hover - Created with Sketch. - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_cebianshouqi.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_cebianshouqi.svg deleted file mode 100644 index 8acbdda5f..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_cebianshouqi.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - 收起 - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_cebianzhankai.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_cebianzhankai.svg deleted file mode 100644 index 58aa3a411..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_cebianzhankai.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - 收起 - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_close_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_close_16.svg deleted file mode 100644 index 9b7ecac32..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_close_16.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - ic_close_16 - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_close_white.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_close_white.svg deleted file mode 100644 index 683b6f28e..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_close_white.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - Combined Shape - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_daorushuju_normal.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_daorushuju_normal.svg deleted file mode 100644 index 309bda2a0..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_daorushuju_normal.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - 画板 - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_daorushuju_pressed.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_daorushuju_pressed.svg deleted file mode 100644 index 5310b8c8e..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_daorushuju_pressed.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - 画板 - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_error_12.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_error_12.svg deleted file mode 100644 index 5c493d041..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_error_12.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - ic_error_12 - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_fangda_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_fangda_16.svg deleted file mode 100644 index ceb6c4944..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_fangda_16.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - ic_fangda_16 - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_json_hover.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_json_hover.svg deleted file mode 100644 index ea70407e1..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_json_hover.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - ic_json_hover - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_liebiaomoshi_black.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_liebiaomoshi_black.svg deleted file mode 100644 index 521a07fa7..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_liebiaomoshi_black.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - 图标 / M / 目录/black - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_liebiaomoshi_white.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_liebiaomoshi_white.svg deleted file mode 100644 index 7fdeb9069..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_liebiaomoshi_white.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - 图标 / M / 目录 - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_loading@2x.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_loading@2x.svg deleted file mode 100644 index 9d6387a56..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_loading@2x.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - ic_loading@2x - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_middle_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_middle_16.svg deleted file mode 100644 index daf8e682f..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_middle_16.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - ic_middle_16 - Created with Sketch. - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_pass.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_pass.svg deleted file mode 100644 index 869206d53..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_pass.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - 通过-M01 - Created with Sketch. - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_progress_done.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_progress_done.svg deleted file mode 100644 index 100aa5fa9..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_progress_done.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - 编组 6 - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_quanping_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_quanping_16.svg deleted file mode 100644 index 2ab27ce86..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_quanping_16.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - ic_quanping_16 - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_question_mark.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_question_mark.svg deleted file mode 100644 index 9b33079ab..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_question_mark.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - 问号-默认-M01 - Created with Sketch. - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_refresh.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_refresh.svg deleted file mode 100644 index 93c1902b5..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_refresh.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - ic_shuaxin - Created with Sketch. - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_renwuguanli_normal.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_renwuguanli_normal.svg deleted file mode 100644 index ec72663da..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_renwuguanli_normal.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - 画板 - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_renwuguanli_pressed.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_renwuguanli_pressed.svg deleted file mode 100644 index f3b72792e..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_renwuguanli_pressed.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - 画板 - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_shuju_normal.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_shuju_normal.svg deleted file mode 100644 index c41c61ebe..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_shuju_normal.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - ic_shuju_normal - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_shuju_pressed.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_shuju_pressed.svg deleted file mode 100644 index 17abce564..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_shuju_pressed.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - ic_shuju_pressed - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_suoxiao_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_suoxiao_16.svg deleted file mode 100644 index 3d082e7fc..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_suoxiao_16.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - ic_suoxiao_16 - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_topback.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_topback.svg deleted file mode 100644 index 39ec0aa63..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_topback.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - Group 2 - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tuichuquanping_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tuichuquanping_16.svg deleted file mode 100644 index 3485c6cd8..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tuichuquanping_16.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - ic_tuichuquanping_16 - Created with Sketch. - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tumoshi_black.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tumoshi_black.svg deleted file mode 100644 index 1d71e4f67..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tumoshi_black.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - bullseye - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tumoshi_white.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tumoshi_white.svg deleted file mode 100644 index a3ce6ccaf..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tumoshi_white.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - bullseye - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tuzhanshi_hover.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tuzhanshi_hover.svg deleted file mode 100644 index de4a22646..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_tuzhanshi_hover.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - ic_tuzhanshi_hover - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_xiazai_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_xiazai_16.svg deleted file mode 100644 index 31dfc3592..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_xiazai_16.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - ic_xiazai_16 - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yingshe_16.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yingshe_16.svg deleted file mode 100644 index 97bbbdafd..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yingshe_16.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - ic_yingshe_16 - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yuanshuju_normal.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yuanshuju_normal.svg deleted file mode 100644 index 367d5d940..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yuanshuju_normal.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - ic_yuanshuju_normal - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yuanshuju_pressed.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yuanshuju_pressed.svg deleted file mode 100644 index dcb23e9b4..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/ic_yuanshuju_pressed.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - ic_yuanshuju_pressed - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/icon_clickarrow_left_grey.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/icon_clickarrow_left_grey.svg deleted file mode 100644 index fa590b9f3..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/icon_clickarrow_left_grey.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - icon_clickarrow_down_grey - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/icon_clickarrow_rigth_grey.svg b/hugegraph-hubble/hubble-fe/src/assets/imgs/icon_clickarrow_rigth_grey.svg deleted file mode 100644 index a5e3e2209..000000000 --- a/hugegraph-hubble/hubble-fe/src/assets/imgs/icon_clickarrow_rigth_grey.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - icon_clickarrow_down_grey - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/assets/layout_circular.svg b/hugegraph-hubble/hubble-fe/src/assets/layout_circular.svg new file mode 100644 index 000000000..f5e4e304c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/assets/layout_circular.svg @@ -0,0 +1,19 @@ + + + diff --git a/hugegraph-hubble/hubble-fe/src/assets/layout_concentric.svg b/hugegraph-hubble/hubble-fe/src/assets/layout_concentric.svg new file mode 100644 index 000000000..8b3f1520e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/assets/layout_concentric.svg @@ -0,0 +1,19 @@ + + + diff --git a/hugegraph-hubble/hubble-fe/src/assets/layout_dagre.svg b/hugegraph-hubble/hubble-fe/src/assets/layout_dagre.svg new file mode 100644 index 000000000..ac97db09d --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/assets/layout_dagre.svg @@ -0,0 +1,19 @@ + + + diff --git a/hugegraph-hubble/hubble-fe/src/assets/layout_force.svg b/hugegraph-hubble/hubble-fe/src/assets/layout_force.svg new file mode 100644 index 000000000..2f2305184 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/assets/layout_force.svg @@ -0,0 +1,19 @@ + + + diff --git a/hugegraph-hubble/hubble-fe/src/assets/layout_grid.svg b/hugegraph-hubble/hubble-fe/src/assets/layout_grid.svg new file mode 100644 index 000000000..06b760838 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/assets/layout_grid.svg @@ -0,0 +1,26 @@ + + + + + + diff --git a/hugegraph-hubble/hubble-fe/src/assets/layout_radial.svg b/hugegraph-hubble/hubble-fe/src/assets/layout_radial.svg new file mode 100644 index 000000000..d38e5caad --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/assets/layout_radial.svg @@ -0,0 +1,25 @@ + + + + + + diff --git a/hugegraph-hubble/hubble-fe/src/assets/imgs/logo.png b/hugegraph-hubble/hubble-fe/src/assets/logo.png similarity index 100% rename from hugegraph-hubble/hubble-fe/src/assets/imgs/logo.png rename to hugegraph-hubble/hubble-fe/src/assets/logo.png diff --git a/hugegraph-hubble/hubble-fe/src/assets/logo_img.png b/hugegraph-hubble/hubble-fe/src/assets/logo_img.png new file mode 100644 index 000000000..a4c74a41b Binary files /dev/null and b/hugegraph-hubble/hubble-fe/src/assets/logo_img.png differ diff --git a/hugegraph-hubble/hubble-fe/src/assets/logo_new.png b/hugegraph-hubble/hubble-fe/src/assets/logo_new.png new file mode 100644 index 000000000..daedcc6f1 Binary files /dev/null and b/hugegraph-hubble/hubble-fe/src/assets/logo_new.png differ diff --git a/hugegraph-hubble/hubble-fe/src/assets/logo_text.png b/hugegraph-hubble/hubble-fe/src/assets/logo_text.png new file mode 100644 index 000000000..296eecca1 Binary files /dev/null and b/hugegraph-hubble/hubble-fe/src/assets/logo_text.png differ diff --git a/hugegraph-hubble/hubble-fe/src/components/App.tsx b/hugegraph-hubble/hubble-fe/src/components/App.tsx deleted file mode 100644 index 73e62091b..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/App.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useEffect } from 'react'; -import { Route, Router } from 'wouter'; - -import { AppBar } from './common'; -import { - GraphManagement, - DataAnalyze, - MetadataConfigs, - ImportTasks, - ImportManager, - JobDetails -} from './graph-management'; -import { - TaskErrorLogs, - JobErrorLogs -} from './graph-management/data-import/import-tasks/error-logs'; -import { AsyncTaskList } from './graph-management'; -import AsyncTaskResult from './graph-management/async-tasks/AsyncTaskResult'; -import GraphManagementSidebar from './graph-management/GraphManagementSidebar'; -import { useLocationWithConfirmation } from '../hooks'; - -const App: React.FC = () => { - return ( -
- - - {/* @ts-ignore */} - - - - - - - - {/* */} - - - - - -
- ); -}; - -export default App; diff --git a/hugegraph-hubble/hubble-fe/src/components/CodeEditor/index.js b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/index.js new file mode 100644 index 000000000..a3fdf574c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/index.js @@ -0,0 +1,100 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {autocompletion} from '@codemirror/autocomplete'; +import {syntaxHighlighting, HighlightStyle} from '@codemirror/language'; +import {basicSetup, EditorView} from 'codemirror'; +import React, {useRef, useEffect} from 'react'; +import {placeholder as cmplaceholder} from '@codemirror/view'; +import syntaxConfig from './syntax'; +import {tags} from '@lezer/highlight'; +import {useTranslation} from 'react-i18next'; + +const CodeEditor = ({value, placeholder, onChange, lang = 'gremlin'}) => { + const {t} = useTranslation(); + const editor = useRef(); + const cm = useRef(); + + useEffect(() => { + const syntax = syntaxConfig[lang] ?? syntaxConfig.default; + + const myCompletions = context => { + let before = context.matchBefore(/\w+/); + if (!context.explicit && !before) { + return null; + } + + return { + from: before ? before.from : context.pos, + options: syntax.hint, + validFor: /^\w*$/, + }; + }; + + const myHighlightStyle = HighlightStyle.define([ + {tag: tags.keyword, color: '#fc6eee'}, + {tag: tags.function, color: '#ff0'}, + ]); + + cm.current = new EditorView({ + extensions: [ + basicSetup, + autocompletion({override: [myCompletions]}), + syntaxHighlighting(myHighlightStyle), + EditorView.updateListener.of(e => { + onChange && onChange(e.state.doc.toString()); + }), + EditorView.theme( + { + '&': { + color: '#000', + }, + '&.cm-focused': { + outline: '0', + }, + '.cm-activeLine': { + 'background-color': 'transparent', + }, + } + ), + cmplaceholder(placeholder ?? t('analysis.query.placeholder')), + ], + parent: editor.current, + }); + + // onChange && EditorView.updateListener.of(e => onChange(e.state.doc.toString())); + + return () => { + cm.current.destroy(); + }; + }, [t, lang, onChange, placeholder]); + + useEffect(() => { + if (value !== null && cm.current.state.doc && value !== cm.current.state.doc.toString()) { + cm.current.dispatch({ + changes: {from: 0, to: cm.current.state.doc.length, insert: value}, + }); + } + }, [value]); + + return ( +
+ ); +}; + +export default CodeEditor; diff --git a/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/cypher.js b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/cypher.js new file mode 100644 index 000000000..e638b5c8b --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/cypher.js @@ -0,0 +1,131 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const hint = [ + // constant/type/class/function + // Clauses + {label: 'CALL', type: 'method'}, + {label: 'CREATE', type: 'method'}, + {label: 'DELETE', type: 'method'}, + {label: 'DETACH', type: 'method'}, + {label: 'FOREACH', type: 'method'}, + {label: 'LOAD', type: 'method'}, + {label: 'MATCH', type: 'method'}, + {label: 'MERGE', type: 'method'}, + {label: 'OPTIONAL MATCH', type: 'method'}, + {label: 'REMOVE', type: 'method'}, + {label: 'RETURN', type: 'method'}, + {label: 'SET', type: 'method'}, + {label: 'START', type: 'method'}, + {label: 'UNION', type: 'method'}, + {label: 'UNWIND', type: 'method'}, + {label: 'WITH', type: 'method'}, + + // Subclauses + {label: 'LIMIT', type: 'constant'}, + {label: 'ORDER BY', type: 'constant'}, + {label: 'SKIP', type: 'constant'}, + {label: 'WHERE', type: 'constant'}, + {label: 'YIELD', type: 'constant'}, + + // Modifiers + {label: 'ASC', type: 'keyword'}, + {label: 'ASCENDING', type: 'keyword'}, + {label: 'ASSERT', type: 'keyword'}, + {label: 'BY', type: 'keyword'}, + {label: 'CSV', type: 'keyword'}, + {label: 'DESC', type: 'keyword'}, + {label: 'DESCENDING', type: 'keyword'}, + {label: 'ON', type: 'keyword'}, + + // Expressions + {label: 'ALL', type: 'keyword'}, + {label: 'CASE', type: 'keyword'}, + {label: 'COUNT', type: 'keyword'}, + {label: 'ELSE', type: 'keyword'}, + {label: 'END', type: 'keyword'}, + {label: 'EXISTS', type: 'keyword'}, + {label: 'THEN', type: 'keyword'}, + {label: 'WHEN', type: 'keyword'}, + + // Operators + {label: 'AND', type: 'keyword'}, + {label: 'AS', type: 'keyword'}, + {label: 'CONTAINS', type: 'keyword'}, + {label: 'DISTINCT', type: 'keyword'}, + {label: 'ENDS', type: 'keyword'}, + {label: 'IN', type: 'keyword'}, + {label: 'IS', type: 'keyword'}, + {label: 'NOT', type: 'keyword'}, + {label: 'OR', type: 'keyword'}, + {label: 'STARTS', type: 'keyword'}, + {label: 'XOR', type: 'keyword'}, + + // Schema + {label: 'CONSTRAINT', type: 'keyword'}, + {label: 'CREATE', type: 'keyword'}, + {label: 'DROP', type: 'keyword'}, + {label: 'EXISTS', type: 'keyword'}, + {label: 'INDEX', type: 'keyword'}, + {label: 'NODE', type: 'keyword'}, + {label: 'KEY', type: 'keyword'}, + {label: 'UNIQUE', type: 'keyword'}, + + // Hints + {label: 'INDEX', type: 'keyword'}, + {label: 'JOIN', type: 'keyword'}, + {label: 'SCAN', type: 'keyword'}, + {label: 'USING', type: 'keyword'}, + + // Literals + {label: 'false', type: 'keyword'}, + {label: 'null', type: 'keyword'}, + {label: 'true', type: 'keyword'}, + + // Reserved for future use + {label: 'ADD', type: 'keyword'}, + {label: 'DO', type: 'keyword'}, + {label: 'FOR', type: 'keyword'}, + {label: 'MANDATORY', type: 'keyword'}, + {label: 'OF', type: 'keyword'}, + {label: 'REQUIRE', type: 'keyword'}, + {label: 'SCALAR', type: 'keyword'}, +]; + +const highlight = [ + {tag: 'CALL', color: '#fc6'}, + {tag: 'CREATE', color: '#fc6'}, + {tag: 'DELETE', color: '#fc6'}, + {tag: 'DETACH', color: '#fc6'}, + {tag: 'FOREACH', color: '#fc6'}, + {tag: 'LOAD', color: '#fc6'}, + {tag: 'MATCH', color: '#fc6'}, + {tag: 'MERGE', color: '#fc6'}, + {tag: 'OPTIONAL MATCH', color: '#fc6'}, + {tag: 'REMOVE', color: '#fc6'}, + {tag: 'RETURN', color: '#fc6'}, + {tag: 'SET', color: '#fc6'}, + {tag: 'START', color: '#fc6'}, + {tag: 'UNION', color: '#fc6'}, + {tag: 'UNWIND', color: '#fc6'}, + {tag: 'WITH', color: '#fc6'}, + // {tag: '', color: '#fc6'}, + // {tag: tags.comment, color: "#f5d", fontStyle: "italic"} +]; + +export {highlight, hint}; diff --git a/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/gremlin.js b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/gremlin.js new file mode 100644 index 000000000..18c80e2af --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/gremlin.js @@ -0,0 +1,176 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +// const hint = [ +// 'g', +// 'graph', +// 'out', 'int', 'both', 'outE', 'inE', 'bothE', 'outV', 'inV', 'bothV', 'otherV', +// 'hasNext', 'next', 'tryNext', 'toList', 'toSet', 'toBulkSet', 'fill', 'iterate', +// 'addV', 'addE', 'aggregate', 'and', 'as', +// 'barrier', 'branch', 'by', +// 'call', 'cap', 'choose', 'coalesce', 'coin', 'connectedComponent', 'constant', 'count', 'cyclicPath', +// 'dedup', 'drop', +// 'E', 'element', 'elementMap', 'emit', 'explain', +// 'fail', 'filter', 'flatMap', 'fold', 'from', +// 'group', 'groupCount', +// 'has', 'hasLabel', 'hasId', 'hasKey', 'hasValue', 'hasNot', +// 'id', 'identity', 'index', 'inject', 'io', 'is', +// 'key', +// 'label', 'limit', 'local', 'loops', +// 'map', 'match', 'math', 'max', 'mean', 'mergeE', 'mergeV', 'min', +// 'none', 'not', +// 'option', 'optional', 'or', 'order', +// 'pageRank', 'path', 'peerPressure', 'profile', 'project', 'program', 'properties', 'property', +// 'propertiesMap', +// 'range', 'read', 'repeat', +// 'sack', 'sample', 'select', 'shortestPath', 'sideEffect', 'simplePath', 'skip', 'subgraph', 'sum', +// 'tail', 'timeLimit', 'to', 'tree', +// 'unfold', 'union', 'until', +// 'V', 'value', 'valueMap', 'values', +// 'where', 'with', 'write', +// ]; + +// const highlight = []; + +// export {hint, highlight}; + + +const hint = [ + {label: 'g', type: 'constant'}, + {label: 'graph', type: 'constant'}, + {label: 'out', type: 'function'}, + {label: 'int', type: 'function'}, + {label: 'both', type: 'function'}, + {label: 'outE', type: 'function'}, + {label: 'inE', type: 'function'}, + {label: 'bothE', type: 'function'}, + {label: 'outV', type: 'function'}, + {label: 'inV', type: 'function'}, + {label: 'bothV', type: 'function'}, + {label: 'otherV', type: 'function'}, + {label: 'hasNext', type: 'function'}, + {label: 'next', type: 'function'}, + {label: 'tryNext', type: 'function'}, + {label: 'toList', type: 'function'}, + {label: 'toSet', type: 'function'}, + {label: 'toBulkSet', type: 'function'}, + {label: 'fill', type: 'function'}, + {label: 'iterate', type: 'function'}, + {label: 'addV', type: 'function'}, + {label: 'addE', type: 'function'}, + {label: 'aggregate', type: 'function'}, + {label: 'and', type: 'function'}, + {label: 'as', type: 'function'}, + {label: 'barrier', type: 'function'}, + {label: 'branch', type: 'function'}, + {label: 'by', type: 'function'}, + {label: 'call', type: 'function'}, + {label: 'cap', type: 'function'}, + {label: 'choose', type: 'function'}, + {label: 'coalesce', type: 'function'}, + {label: 'coin', type: 'function'}, + {label: 'connectedComponent', type: 'function'}, + {label: 'constant', type: 'function'}, + {label: 'count', type: 'function'}, + {label: 'cyclicPath', type: 'function'}, + {label: 'dedup', type: 'function'}, + {label: 'drop', type: 'function'}, + {label: 'E', type: 'function'}, + {label: 'element', type: 'function'}, + {label: 'elementMap', type: 'function'}, + {label: 'emit', type: 'function'}, + {label: 'explain', type: 'function'}, + {label: 'fail', type: 'function'}, + {label: 'filter', type: 'function'}, + {label: 'flatMap', type: 'function'}, + {label: 'fold', type: 'function'}, + {label: 'from', type: 'function'}, + {label: 'group', type: 'function'}, + {label: 'groupCount', type: 'function'}, + {label: 'has', type: 'function'}, + {label: 'hasLabel', type: 'function'}, + {label: 'hasId', type: 'function'}, + {label: 'hasKey', type: 'function'}, + {label: 'hasValue', type: 'function'}, + {label: 'hasNot', type: 'function'}, + {label: 'id', type: 'function'}, + {label: 'identity', type: 'function'}, + {label: 'index', type: 'function'}, + {label: 'inject', type: 'function'}, + {label: 'io', type: 'function'}, + {label: 'is', type: 'function'}, + {label: 'key', type: 'function'}, + {label: 'label', type: 'function'}, + {label: 'limit', type: 'function'}, + {label: 'local', type: 'function'}, + {label: 'loops', type: 'function'}, + {label: 'map', type: 'function'}, + {label: 'match', type: 'function'}, + {label: 'math', type: 'function'}, + {label: 'max', type: 'function'}, + {label: 'mean', type: 'function'}, + {label: 'mergeE', type: 'function'}, + {label: 'mergeV', type: 'function'}, + {label: 'min', type: 'function'}, + {label: 'none', type: 'function'}, + {label: 'not', type: 'function'}, + {label: 'option', type: 'function'}, + {label: 'optional', type: 'function'}, + {label: 'or', type: 'function'}, + {label: 'order', type: 'function'}, + {label: 'pageRank', type: 'function'}, + {label: 'path', type: 'function'}, + {label: 'peerPressure', type: 'function'}, + {label: 'profile', type: 'function'}, + {label: 'project', type: 'function'}, + {label: 'program', type: 'function'}, + {label: 'properties', type: 'function'}, + {label: 'property', type: 'function'}, + {label: 'propertiesMap', type: 'function'}, + {label: 'range', type: 'function'}, + {label: 'read', type: 'function'}, + {label: 'repeat', type: 'function'}, + {label: 'sack', type: 'function'}, + {label: 'sample', type: 'function'}, + {label: 'select', type: 'function'}, + {label: 'shortestPath', type: 'function'}, + {label: 'sideEffect', type: 'function'}, + {label: 'simplePath', type: 'function'}, + {label: 'skip', type: 'function'}, + {label: 'subgraph', type: 'function'}, + {label: 'sum', type: 'function'}, + {label: 'tail', type: 'function'}, + {label: 'timeLimit', type: 'function'}, + {label: 'to', type: 'function'}, + {label: 'tree', type: 'function'}, + {label: 'unfold', type: 'function'}, + {label: 'union', type: 'function'}, + {label: 'until', type: 'function'}, + {label: 'V', type: 'function'}, + {label: 'value', type: 'function'}, + {label: 'valueMap', type: 'function'}, + {label: 'values', type: 'function'}, + {label: 'where', type: 'function'}, + {label: 'with', type: 'function'}, + {label: 'write', type: 'function'}, +]; + +const highlight = []; + +export {hint, highlight}; + diff --git a/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/index.js b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/index.js new file mode 100644 index 000000000..b82c9d707 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/CodeEditor/syntax/index.js @@ -0,0 +1,22 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import * as gremlin from './gremlin'; +import * as cypher from './cypher'; + +export default {gremlin, cypher, default: {hint: [], highlight: []}}; diff --git a/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.js b/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.js new file mode 100644 index 000000000..707d340e0 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.js @@ -0,0 +1,103 @@ + +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {useCallback, useEffect, useId, useRef, useState} from 'react'; +import {ChromePicker} from 'react-color'; +import styles from './index.module.scss'; +import {useTranslation} from 'react-i18next'; + +const InputColorSelect = ({value, onChange, disable}) => { + const {t} = useTranslation(); + const [visible, setVisible] = useState(false); + const [color, setColor] = useState('#5c73e6'); + const triggerRef = useRef(null); + const pickerId = useId(); + + const showPicker = useCallback(() => { + if (disable) { + return; + } + + setVisible(true); + }, [disable]); + + const hidePicker = useCallback(() => { + if (disable) { + return; + } + + setVisible(false); + triggerRef.current?.focus(); + }, [disable]); + + const handleKeyDown = useCallback(event => { + if (event.key === 'Escape' && visible) { + event.preventDefault(); + hidePicker(); + } + }, [hidePicker, visible]); + + const handleClick = useCallback(color => { + if (disable) { + return; + } + + setColor(color.hex); + + onChange?.(color.hex); + }, [onChange, disable]); + + useEffect(() => { + if (value) { + setColor(value); + } + }, [value]); + + return ( +
+ {disable ?
: null} +
+
+ {visible ? ( +
+
+ ) : null} +
+ ); +}; + +export {InputColorSelect}; diff --git a/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.module.scss new file mode 100644 index 000000000..742d6af26 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.module.scss @@ -0,0 +1,71 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.wrap { + line-height: 0; + position: relative; +} + +.disable { + // border-top: 1px solid #FF0000; + width: 60px; + // transform: rotate(27deg); + position: absolute; + z-index: 2; + height: 32px; + top: 0; + // top: 17px; + // left: -2px; + // display: inline-block; + background: linear-gradient(28deg, transparent 49%, deeppink 49%, deeppink 50.5%, transparent 50.5%); +} + +.color { + width: 46px; + height: 18px; + border-radius: 2px; + border: 0; + padding: 0; + cursor: pointer; +} + +.swatch { + padding: 6px; + background: #fff; + border-radius: 2px; + // box-shadow: 0 0 0 1px rgba(0, 0, 0, .1); + border: 1px solid #d9d9d9; + display: inline-block; + cursor: pointer; +} + +.popover { + position: absolute; + z-index: 2; +} + +.cover { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + border: 0; + padding: 0; + background: transparent; +} diff --git a/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.test.js b/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.test.js new file mode 100644 index 000000000..6a6f1009e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ColorSelect/index.test.js @@ -0,0 +1,45 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {InputColorSelect} from './index'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({t: key => key}), +})); + +jest.mock('react-color', () => ({ + ChromePicker: () =>
picker content
, +})); + +test('opens and closes the color picker with keyboard state and focus recovery', async () => { + render(); + + const trigger = screen.getByRole('button', {name: 'style_config.color_picker'}); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + + trigger.focus(); + await userEvent.keyboard('{Enter}'); + expect(screen.getByText('picker content')).toBeInTheDocument(); + expect(trigger).toHaveAttribute('aria-expanded', 'true'); + + await userEvent.keyboard('{Escape}'); + expect(screen.queryByText('picker content')).not.toBeInTheDocument(); + expect(trigger).toHaveFocus(); +}); diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/EditLayer.js b/hugegraph-hubble/hubble-fe/src/components/ERView/EditLayer.js new file mode 100644 index 000000000..d3b161244 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/EditLayer.js @@ -0,0 +1,90 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {Form, Modal, Select} from 'antd'; +import vertexData from './data/vertex.json'; +import edgeData from './data/edge.json'; +import {useCallback} from 'react'; +import {useTranslation} from 'react-i18next'; + +const EditVertexLayer = ({open, onCancle: onCancel, onChange}) => { + const {t} = useTranslation(); + const [form] = Form.useForm(); + + const setVertex = useCallback((_, item) => { + form.setFieldValue('vertex', item.info); + }, [form]); + + const onFinish = useCallback(() => { + onChange(form.getFieldValue('vertex')); + onCancel(); + }, [form, onChange, onCancel]); + + return ( + +
+ + ({label: item.name, value: item.name, info: item}))} + onChange={setEdge} + /> + + + +
+ ); +}; + +export {EditVertexLayer, EditEdgeLayer}; diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/config.js b/hugegraph-hubble/hubble-fe/src/components/ERView/config.js new file mode 100644 index 000000000..6dc4d81fc --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/config.js @@ -0,0 +1,307 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +const LINE_HEIGHT = 24; +const NODE_WIDTH = 150; + +const erRectConfig = { + inherit: 'rect', + markup: [ + { + tagName: 'rect', + selector: 'body', + }, + { + tagName: 'text', + selector: 'label', + }, + // { + // tagName: 'g', + // attrs: { + // class: 'btn add', + // }, + // children: [ + // { + // tagName: 'circle', + // attrs: { + // class: 'add', + // }, + // }, + // { + // tagName: 'text', + // attrs: { + // class: 'add', + // }, + // }, + // ], + // }, + ], + tools: [ + { + name: 'button', + args: { + x: 10, + y: 10, + markup: [ + // { + // tagName: 'circle', + // attrs: { + // stroke: '#fff', + // fill: 'transparent', + // cursor: 'pointer', + // 'stroke-width': 1, + // r: 8, + // }, + // }, + { + tagName: 'text', + textContent: '∅', + attrs: { + fontSize: 16, + fontWeight: 800, + x: -4, + y: 6, + fill: '#fff', + fontFamily: 'Times New Roman', + cursor: 'pointer', + }, + }, + ], + }, + }, + { + name: 'button-remove', + args: { + x: '100%', + y: 0, + offset: {x: 0, y: 0}, + }, + }, + ], + attrs: { + rect: { + strokeWidth: 1, + stroke: '#5F95FF', + fill: '#5F95FF', + }, + label: { + fontWeight: 'bold', + fill: '#ffffff', + fontSize: 12, + }, + '.btn.add': { + 'refDx': -16, + 'refY': 12, + 'event': 'node:add', + }, + '.btn.del': { + 'refDx': -44, + 'refY': 16, + 'event': 'node:delete', + }, + '.btn > circle': { + 'r': 8, + 'fill': 'transparent', + 'stroke': '#fff', + 'strokeWidth': 1, + }, + '.btn.add > text': { + 'fontSize': 16, + 'fontWeight': 800, + 'fill': '#fff', + 'x': -75, + 'y': -12, + 'fontFamily': 'Times New Roman', + 'text': '+', + }, + '.btn.del > text': { + 'fontSize': 28, + 'fontWeight': 500, + 'fill': '#fff', + 'x': -4.5, + 'y': 6, + 'fontFamily': 'Times New Roman', + 'text': '-', + }, + }, + ports: { + groups: { + list: { + markup: [ + { + tagName: 'rect', + selector: 'portBody', + }, + { + tagName: 'text', + selector: 'portNameLabel', + }, + { + tagName: 'text', + selector: 'portTypeLabel', + }, + ], + attrs: { + portBody: { + width: NODE_WIDTH, + height: LINE_HEIGHT, + strokeWidth: 1, + stroke: '#5F95FF', + fill: '#EFF4FF', + magnet: true, + }, + portNameLabel: { + ref: 'portBody', + refX: 6, + refY: 6, + fontSize: 10, + }, + portTypeLabel: { + ref: 'portBody', + refX: 95, + refY: 6, + fontSize: 10, + }, + }, + position: 'erPortPosition', + }, + title: { + markup: [ + { + tagName: 'rect', + selector: 'portBody', + }, + { + tagName: 'text', + selector: 'portNameLabel', + }, + { + tagName: 'text', + selector: 'portTypeLabel', + }, + ], + attrs: { + portBody: { + width: NODE_WIDTH, + height: LINE_HEIGHT, + strokeWidth: 1, + stroke: '#5F95FF', + fill: '#FFFF00', + magnet: true, + }, + portNameLabel: { + ref: 'portBody', + refX: 6, + refY: 6, + fontSize: 10, + }, + portTypeLabel: { + ref: 'portBody', + refX: 95, + refY: 6, + fontSize: 10, + }, + }, + position: 'erPortPosition', + }, + }, + }, +}; + +const erRectHeadConfig = { + inherit: 'rect', + markup: [ + { + tagName: 'rect', + selector: 'body', + }, + { + tagName: 'text', + selector: 'label', + }, + ], + attrs: { + rect: { + strokeWidth: 1, + stroke: '#5F95FF', + fill: '#5F95FF', + }, + label: { + fontWeight: 'bold', + fill: '#ffffff', + fontSize: 12, + }, + }, + ports: { + groups: { + list: { + markup: [ + { + tagName: 'rect', + selector: 'portBody', + }, + { + tagName: 'text', + selector: 'portNameLabel', + }, + { + tagName: 'text', + selector: 'portTypeLabel', + }, + ], + attrs: { + portBody: { + width: NODE_WIDTH, + height: LINE_HEIGHT, + strokeWidth: 1, + stroke: '#5F95FF', + fill: '#EFF4FF', + magnet: true, + }, + portNameLabel: { + ref: 'portBody', + refX: 6, + refY: 6, + fontSize: 10, + }, + portTypeLabel: { + ref: 'portBody', + refX: 95, + refY: 6, + fontSize: 10, + }, + }, + position: 'erPortPosition', + }, + }, + }, +}; + +const erPortPosition = portsPositionArgs => { + return portsPositionArgs.map((a, index) => { + return { + position: { + x: 0, + y: (index + 1) * LINE_HEIGHT, + }, + angle: 0, + }; + }); +}; + +export {erRectConfig, erRectHeadConfig, erPortPosition}; diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/data/edge.json b/hugegraph-hubble/hubble-fe/src/components/ERView/data/edge.json new file mode 100644 index 000000000..e18787291 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/data/edge.json @@ -0,0 +1,232 @@ +[ + { + "name": "父子", + "source_label": "男人", + "target_label": "男人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "父女", + "source_label": "男人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "母子", + "source_label": "女人", + "target_label": "男人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "母女", + "source_label": "女人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "妻", + "source_label": "男人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "妾", + "source_label": "男人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "相恋", + "source_label": "男人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "朋友", + "source_label": "男人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "姐妹", + "source_label": "女人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "丫环", + "source_label": "男人", + "target_label": "女人", + "link_multi_times": false, + "properties": [], + "sort_keys": [], + "property_indexes": [], + "open_label_index": true, + "style": { + "color": "#5C73E6", + "with_arrow": true, + "line_type": "SOLID", + "thickness": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + } +] diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/data/group.json b/hugegraph-hubble/hubble-fe/src/components/ERView/data/group.json new file mode 100644 index 000000000..91cbce270 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/data/group.json @@ -0,0 +1,95 @@ +{ + "markup": [ + { + "tagName": "rect", + "selector": "portBody" + }, + { + "tagName": "text", + "selector": "portNameLabel" + }, + { + "tagName": "text", + "selector": "portTypeLabel" + }, + { + "tagName": "g", + "attrs": { + "class": "btn add" + }, + "children": [ + { + "tagName": "circle", + "attrs": { + "class": "add" + } + }, + { + "tagName": "text", + "attrs": { + "class": "add" + } + } + ] + } + ], + "attrs": { + "portBody": { + "width": 150, + "height": 24, + "strokeWidth": 1, + "stroke": "#5F95FF", + "fill": "#EFF4FF", + "magnet": true + }, + "portNameLabel": { + "refX": 6, + "refY": 6, + "fontSize": 10, + "text": "ID" + }, + "portTypeLabel": { + "refX": 95, + "refY": 6, + "fontSize": 10, + "text": "" + }, + "rect": { + "fill": "#FAFAFA" + }, + ".btn.add": { + "refDx": -16, + "refY": 16, + "event": "node:add" + }, + ".btn.del": { + "refDx": -44, + "refY": 16, + "event": "node:delete" + }, + ".btn > circle": { + "r": 10, + "fill": "transparent", + "stroke": "#fff", + "strokeWidth": 1 + }, + ".btn.add > text": { + "fontSize": 20, + "fontWeight": 800, + "fill": "#fff", + "x": -5.5, + "y": 7, + "fontFamily": "Times New Roman", + "text": "+" + }, + ".btn.del > text": { + "fontSize": 28, + "fontWeight": 500, + "fill": "#fff", + "x": -4.5, + "y": 6, + "fontFamily": "Times New Roman", + "text": "-" + } + } +} \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/data/property.json b/hugegraph-hubble/hubble-fe/src/components/ERView/data/property.json new file mode 100644 index 000000000..02c9b987f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/data/property.json @@ -0,0 +1,32 @@ +[ + { + "name": "姓名", + "data_type": "TEXT", + "cardinality": "SINGLE", + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "性别", + "data_type": "TEXT", + "cardinality": "SINGLE", + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "年龄", + "data_type": "INT", + "cardinality": "SINGLE", + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "特点", + "data_type": "TEXT", + "cardinality": "SINGLE", + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "亲疏", + "data_type": "TEXT", + "cardinality": "SINGLE", + "create_time": "1970-01-01 08:00:00" + } +] \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/data/test.json b/hugegraph-hubble/hubble-fe/src/components/ERView/data/test.json new file mode 100644 index 000000000..4e18a159b --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/data/test.json @@ -0,0 +1,295 @@ +[ + { + "id": "1", + "shape": "er-rect", + "label": "男人", + "width": 150, + "height": 24, + "position": { + "x": 24, + "y": 150 + }, + "ports": [ + { + "id": "1", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "ID" + }, + "portTypeLabel": { + "text": "" + }, + "rect": { + "fill": "#FAFAFA" + } + } + }, + { + "id": "1-1", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "ID" + }, + "portTypeLabel": { + "text": "" + } + } + }, + { + "id": "1-2", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Property" + }, + "portTypeLabel": { + "text": "特点" + } + } + }, + { + "id": "1-3", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Property" + }, + "portTypeLabel": { + "text": "亲疏" + } + } + }, + { + "id": "1-4", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Property" + }, + "portTypeLabel": { + "text": "性别" + } + } + }, + { + "id": "1-5", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Property" + }, + "portTypeLabel": { + "text": "年龄" + } + } + }, + { + "id": "1-6", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Value" + }, + "portTypeLabel": { + "text": "aa->bb" + } + } + }, + { + "id": "1-7", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Value" + }, + "portTypeLabel": { + "text": "aa1->bb1" + } + }, + "tools": [{ + "name": "node-editor" + }] + }, + { + "id": "1-8", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Value" + }, + "portTypeLabel": { + "text": "aa2->bb2" + } + } + } + ] + }, + { + "id": "2", + "shape": "er-head-rect", + "label": "head", + "width": 150, + "height": 24, + "position": { + "x": 250, + "y": 210 + }, + "ports": [ + { + "id": "2-1", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Col-1" + }, + "portTypeLabel": { + "text": "STRING" + } + } + }, + { + "id": "2-2", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Col-2" + }, + "portTypeLabel": { + "text": "STRING" + } + } + }, + { + "id": "2-3", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Col-3" + }, + "portTypeLabel": { + "text": "STRING" + } + } + }, + { + "id": "2-4", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Col-4" + }, + "portTypeLabel": { + "text": "STRING" + } + } + }, + { + "id": "2-5", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Col-5" + }, + "portTypeLabel": { + "text": "STRING" + } + } + } + ] + }, + { + "id": "3", + "shape": "er-rect", + "label": "父子", + "width": 150, + "height": 24, + "position": { + "x": 480, + "y": 350 + }, + "ports": [ + { + "id": "3-1", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "起点ID" + } + } + }, + { + "id": "3-2", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "终点ID" + } + } + }, + { + "id": "3-3", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Property" + }, + "portTypeLabel": { + "text": "年龄" + } + } + }, + { + "id": "3-4", + "group": "list", + "attrs": { + "portNameLabel": { + "text": "Value" + }, + "portTypeLabel": { + "text": "ede1->nn1" + } + } + } + ] + }, + { + "id": "4", + "shape": "edge", + "source": { + "cell": "1", + "port": "1-1" + }, + "target": { + "cell": "2", + "port": "2-3" + }, + "attrs": { + "line": { + "stroke": "#A2B1C3", + "strokeWidth": 2 + } + }, + "zIndex": 0 + }, + { + "id": "5", + "shape": "edge", + "source": { + "cell": "3", + "port": "3-1" + }, + "target": { + "cell": "2", + "port": "2-4" + }, + "attrs": { + "line": { + "stroke": "#A2B1C3", + "strokeWidth": 2 + } + }, + "zIndex": 0 + } +] \ No newline at end of file diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/data/vertex.json b/hugegraph-hubble/hubble-fe/src/components/ERView/data/vertex.json new file mode 100644 index 000000000..cf9d0cb9d --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/data/vertex.json @@ -0,0 +1,123 @@ +[ + { + "name": "男人", + "id_strategy": "PRIMARY_KEY", + "properties": [ + { + "name": "特点", + "nullable": false + }, + { + "name": "亲疏", + "nullable": false + }, + { + "name": "性别", + "nullable": false + }, + { + "name": "姓名", + "nullable": false + }, + { + "name": "年龄", + "nullable": false + } + ], + "primary_keys": [ + "姓名" + ], + "property_indexes": [], + "open_label_index": true, + "style": { + "icon": "", + "color": "#5C73E6", + "size": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "女人", + "id_strategy": "PRIMARY_KEY", + "properties": [ + { + "name": "特点", + "nullable": false + }, + { + "name": "亲疏", + "nullable": false + }, + { + "name": "性别", + "nullable": false + }, + { + "name": "姓名", + "nullable": false + }, + { + "name": "年龄", + "nullable": false + } + ], + "primary_keys": [ + "姓名" + ], + "property_indexes": [], + "open_label_index": true, + "style": { + "icon": "", + "color": "#5C73E6", + "size": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + }, + { + "name": "机构", + "id_strategy": "PRIMARY_KEY", + "properties": [ + { + "name": "特点", + "nullable": false + }, + { + "name": "亲疏", + "nullable": false + }, + { + "name": "姓名", + "nullable": false + } + ], + "primary_keys": [ + "姓名" + ], + "property_indexes": [], + "open_label_index": true, + "style": { + "icon": "", + "color": "#5C73E6", + "size": "NORMAL", + "display_fields": [ + "~id" + ], + "join_symbols": [ + "-" + ] + }, + "create_time": "1970-01-01 08:00:00" + } +] diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/index.js b/hugegraph-hubble/hubble-fe/src/components/ERView/index.js new file mode 100644 index 000000000..6db502e36 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/index.js @@ -0,0 +1,325 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {Graph, Shape} from '@antv/x6'; +import {Menu, Toolbar} from '@antv/x6-react-components'; +import {useCallback, useEffect, useRef, useState} from 'react'; +import { + ZoomInOutlined, + ZoomOutOutlined, + RedoOutlined, + UndoOutlined, + DeleteOutlined, +} from '@ant-design/icons'; +import style from './index.module.scss'; +import testData from './data/test.json'; +import {erRectConfig, erRectHeadConfig, erPortPosition} from './config'; +import '@antv/x6-react-components/es/menu/style/index.css'; +import '@antv/x6-react-components/es/toolbar/style/index.css'; +import {EditVertexLayer, EditEdgeLayer} from './EditLayer'; +import {setCell} from './utils'; +import {useTranslation} from 'react-i18next'; + +Graph.registerPortLayout('erPortPosition', erPortPosition); +Graph.registerNode('er-rect', erRectConfig, true); +Graph.registerNode('er-head-rect', erRectHeadConfig, true); + +const ERView = () => { + const container = useRef(null); + const graph = useRef(null); + const {t} = useTranslation(); + const [vertexVisible, setVertexVisible] = useState(false); + const [edgeVisible, setEdgeVisible] = useState(false); + + useEffect(() => { + graph.current = new Graph({ + container: container.current, + grid: true, + history: true, + // selecting: true, + connecting: { + allowMulti: 'withPort', + allowBlank: false, + allowLoop: false, + allowNode: false, + allowEdge: false, + snap: true, + router: { + name: 'er', + args: { + offset: 25, + direction: 'H', + }, + }, + createEdge() { + return new Shape.Edge({ + // tools: [ + // { + // name: 'button-remove', + // args: { + // distance: -40, + // }, + // }, + // ], + attrs: { + line: { + stroke: '#A2B1C3', + strokeWidth: 2, + }, + }, + }); + }, + }, + }); + + const cells = []; + testData.forEach(item => { + if (item.shape === 'edge') { + cells.push(graph.current.createEdge(item)); + } + else { + cells.push(graph.current.createNode(item)); + } + }); + graph.current.resetCells(cells); + graph.current.zoomToFit({padding: 10, maxScale: 1}); + + graph.current.on('node:add', ({e, node}) => { + e.stopPropagation(); + void node; + // const member = createNode( + // 'Employee', + // 'New Employee', + // Math.random() < 0.5 ? male : female, + // ); + // graph.freeze(); + // graph.addCell([member, createEdge(node, member)]) + // layout(); + }); + + graph.current.on('node:delete', ({e, node}) => { + e.stopPropagation(); + void node; + // graph.freeze(); + // graph.removeCell(node); + // layout(); + }); + + graph.current.on('edge:mouseenter', ({cell}) => { + cell.addTools([ + { + name: 'target-arrowhead', + args: { + attrs: { + fill: 'red', + }, + }, + }, + { + name: 'button-remove', + args: { + distance: 0.7, + }, + }, + { + name: 'button', + args: { + distance: 0.5, + onClick: ({cell}) => { + void cell; + }, + markup: [ + { + tagName: 'circle', + selector: 'button', + attrs: { + r: 10, + stroke: '#A2B1C3', + strokeWidth: 1, + fill: 'white', + cursor: 'pointer', + }, + }, + { + tagName: 'g', + attrs: { + transform: 'translate(-8, -9)', + cursor: 'pointer', + }, + children: [ + { + tagName: 'svg', + attrs: { + width: 16, + height: 16, + viewBox: '0 0 1024 1024', + }, + children: [ + { + tagName: 'path', + attrs: { + // eslint-disable-next-line max-len + d: 'M257.7 752c2 0 4-0.2 6-0.5L431.9 722c2-0.4 3.9-1.3 5.3-2.8l423.9-423.9c3.9-3.9 3.9-10.2 0-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2c-1.9 11.1 1.5 21.9 9.4 29.8 6.6 6.4 14.9 9.9 23.8 9.9z m67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z', + fill: '#A2B1C3', + }, + }, + ], + }, + ], + }, + // { + // tagName: 'text', + // textContent: , + // selector: 'icon', + // attrs: { + // fill: '#fe854f', + // fontSize: 10, + // textAnchor: 'middle', + // pointerEvents: 'none', + // y: '0.3em', + // }, + // }, + ], + }, + }, + ]); + }); + + graph.current.on('edge:mouseleave', ({cell}) => { + cell.removeTools(); + }); + }, []); + + const addVertex = useCallback(vertex => { + graph.current.addNode(graph.current.createNode(setCell(t, vertex))); + }, [t]); + + const addEdge = useCallback(edge => { + graph.current.addNode(graph.current.createNode(setCell(t, edge, 'edge'))); + }, [t]); + + const showVertex = useCallback(() => { + setVertexVisible(true); + }, []); + + const hideVertex = useCallback(() => { + setVertexVisible(false); + }, []); + + const showEdge = useCallback(() => { + setEdgeVisible(true); + }, []); + + const hideEdge = useCallback(() => { + setEdgeVisible(false); + }, []); + + const handleZoomIn = useCallback(() => { + graph.current?.zoom(0.5); + }, []); + + const handleZoomOut = useCallback(() => { + graph.current?.zoom(-0.5); + }, []); + + const handleUndo = useCallback(() => { + graph.current?.history.undo(); + }, []); + + const handleRedo = useCallback(() => { + graph.current?.history.redo(); + }, []); + + const Item = Toolbar.Item; + const Group = Toolbar.Group; + const vertexMenu = ( + + {t('ERView.vertex.create')} + + {t('ERView.vertex.v1name')} + {t('ERView.vertex.v2name')} + + ); + + const edgeMenu = ( + + {t('ERView.edge.create')} + + {t('ERView.edge.e1name')} + {t('ERView.edge.e2name')} + + ); + + return ( +
+ + + } + onClick={handleZoomIn} + /> + } + onClick={handleZoomOut} + /> + + + } + onClick={handleUndo} + /> + } + onClick={handleRedo} + /> + + + } disabled tooltip="Delete (Delete)" /> + + + + + + + + + +
+ + +
+ ); +}; + +export default ERView; diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/ERView/index.module.scss new file mode 100644 index 000000000..5cc07c55c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/index.module.scss @@ -0,0 +1,31 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.content { + flex: 1; + height: 540px; + margin-left: 8px; + margin-right: 8px; + box-shadow: 0 0 10px 1px #e9e9e9; +} + +.main { + text-align: center; + margin-top: 20px; + border: 1px solid #e9e9e9; +} diff --git a/hugegraph-hubble/hubble-fe/src/components/ERView/utils.js b/hugegraph-hubble/hubble-fe/src/components/ERView/utils.js new file mode 100644 index 000000000..256677db7 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ERView/utils.js @@ -0,0 +1,117 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + + +const setPropertyRow = item => { + return {...item, attr: {...item.attr, rect: {fill: '#FAFAFA'}}}; +}; + +const setValueRow = item => { + return {...item, attr: {...item.attr, rect: {fill: '#ACACAC'}}}; +}; + +const setCell = (t, cell, type) => { + const idList = []; + if (type === 'edge') { + idList.push( + { + id: `${cell.name}-source`, + group: 'list', + attrs: { + portNameLabel: { + text: t('ERView.edge.start'), + }, + portTypeLabel: { + text: '', + }, + rect: { + fill: '#FAFAFA', + }, + }, + } + ); + + idList.push( + { + id: `${cell.name}-target`, + group: 'list', + attrs: { + portNameLabel: { + text: t('ERView.edge.end'), + }, + portTypeLabel: { + text: '', + }, + rect: { + fill: '#FAFAFA', + }, + }, + } + ); + } + else { + idList.push( + { + id: `${cell.name}-ID`, + group: 'list', + attrs: { + portNameLabel: { + text: 'ID', + }, + portTypeLabel: { + text: '', + }, + rect: { + fill: '#FAFAFA', + }, + }, + } + ); + } + + const propertyList = cell.properties.map(item => ({ + id: `${cell.name}-${item.name}`, + group: 'list', + attrs: { + portNameLabel: { + text: 'Property', + }, + portTypeLabel: { + text: item.name, + }, + }, + })); + + return { + id: cell.name, + shape: 'er-rect', + label: cell.name, + width: 150, + height: 24, + position: { + x: 4, + y: 150, + }, + ports: [ + ...idList, + ...propertyList, + ], + }; +}; + +export {setPropertyRow, setValueRow, setCell}; diff --git a/hugegraph-hubble/hubble-fe/src/components/ExecutionContent/index.js b/hugegraph-hubble/hubble-fe/src/components/ExecutionContent/index.js new file mode 100644 index 000000000..cc856f441 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ExecutionContent/index.js @@ -0,0 +1,69 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 折叠组件 + */ + +import React, {useState, useCallback} from 'react'; +import Highlighter from 'react-highlight-words'; + +import {UpOutlined, DownOutlined} from '@ant-design/icons'; +import c from './index.module.scss'; + +const ExecutionContent = ({content, highlightText}) => { + + const [isExpand, switchExpand] = useState(false); + + const onToggleCollapse = useCallback(() => { + switchExpand(prev => !prev); + }, []); + + const icon = isExpand ? : ; + const contentElement = isExpand ? ( + <> + + + ) : ( + <> + + + ); + + return ( +
+ {icon} + {contentElement} +
+ ); +}; + +export default ExecutionContent; diff --git a/hugegraph-hubble/hubble-fe/src/components/ExecutionContent/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/ExecutionContent/index.module.scss new file mode 100644 index 000000000..721a07bb9 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ExecutionContent/index.module.scss @@ -0,0 +1,22 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.highlight { + color: #1890ff; + background-color: #fff; +} diff --git a/hugegraph-hubble/hubble-fe/src/components/FormListAction/index.js b/hugegraph-hubble/hubble-fe/src/components/FormListAction/index.js new file mode 100644 index 000000000..e28cac4fd --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/FormListAction/index.js @@ -0,0 +1,41 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {Button} from 'antd'; +import {useCallback} from 'react'; + +const FormListRemove = ({remove, index, afterRemove, children}) => { + const handleClick = useCallback(() => { + remove(index); + afterRemove?.(); + }, [afterRemove, index, remove]); + + return ; +}; + +const FormListAdd = ({add, children}) => { + const handleClick = useCallback(() => add(), [add]); + + return ( + + ); +}; + +export {FormListAdd, FormListRemove}; diff --git a/hugegraph-hubble/hubble-fe/src/components/GraphinView/index.js b/hugegraph-hubble/hubble-fe/src/components/GraphinView/index.js new file mode 100644 index 000000000..83a1d1074 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/GraphinView/index.js @@ -0,0 +1,67 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {useCallback} from 'react'; +import Graphin, {Behaviors} from '@antv/graphin'; + +const GraphView = ({data, width, height, layout, style, onClick, config, behaviors}) => { + // const [graphData, setGraphData] = useState([]); + + const {DragCanvas, ZoomCanvas, DragNode, ClickSelect, Hoverable} = Behaviors; + const graphinLayout = { + type: 'graphin-force', + animation: false, + ...layout, + // type: 'preset', + }; + + const handleClickSelect = useCallback(evt => { + const {item} = evt; + const {id, type} = item._cfg; + const model = item.getModel(); + + typeof onClick === 'function' && onClick(id, type, model.data, model, item, evt); + }, [onClick]); + + return ( +
+ + + + + + + + {/* */} + +
+ ); +}; + +export default GraphView; diff --git a/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.js b/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.js new file mode 100644 index 000000000..cf78ab08a --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.js @@ -0,0 +1,107 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import React, {useEffect, useMemo, useState} from 'react'; +import * as AntIcon from '@ant-design/icons'; +import {Popover, Input} from 'antd'; +import classnames from 'classnames'; + +import {iconsMap} from '../../utils/constants'; + +import c from './index.module.scss'; +import {useTranslation} from 'react-i18next'; + +const DEFAULT_ICON = 'UserOutlined'; + +const IconSelect = props => { + const { + value, + onChange, + disabled, + } = props; + const {t} = useTranslation(); + const [icon, setIcon] = useState(); + + useEffect( + () => { + value && setIcon(value); + }, + [value] + ); + + const handleClickIconCallbacks = useMemo( + () => { + const icons = Object.keys(iconsMap); + const callbacks = {}; + for (const item of icons) { + callbacks[item] = () => { + setIcon(item); + onChange(item); + }; + } + return callbacks; + }, + [onChange] + ); + + const renderIcons = () => { + const icons = Object.keys(iconsMap); + return icons.map(item => { + const Icon = AntIcon[item]; + const iconClassName = classnames( + c.icon, + {[c.iconSelected]: item === icon} + ); + return ( +
+ +
+ ); + }); + }; + + if (disabled) { + return ( + + ); + } + + const CurrentIcon = AntIcon[icon || DEFAULT_ICON]; + return ( + {renderIcons()}
} + > + } + /> + + ); +}; + +export default IconSelect; diff --git a/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.module.scss new file mode 100644 index 000000000..0f0239bd5 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.module.scss @@ -0,0 +1,47 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.iconsWrapper { + width: 252px; + display: flex; + flex-wrap: wrap; + margin-right: -14px; + margin-bottom: -14px; + + .icon { + width: 28px; + height: 28px; + margin: 0 14px 14px 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all .3s; + border: 1px solid #999999; + + &:hover { + transform: scale(1.2); + } + } + + .iconSelected { + border: 1px solid #999999; + box-shadow: 0 0 5px 0 #999999; + } +} diff --git a/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.test.js b/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.test.js new file mode 100644 index 000000000..f7cf20324 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/IconSelect/index.test.js @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import fs from 'fs'; +import path from 'path'; + +test('disabled icon selector uses localized placeholder without Chinese copy', () => { + const source = fs.readFileSync(path.join(__dirname, 'index.js'), 'utf8'); + + expect(source).not.toMatch(/[\u3400-\u9fff]/u); + expect(source).toContain("placeholder={t('selector.placeholder')}"); +}); diff --git a/hugegraph-hubble/hubble-fe/src/components/KeyboardAction/index.js b/hugegraph-hubble/hubble-fe/src/components/KeyboardAction/index.js new file mode 100644 index 000000000..628236f9e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/KeyboardAction/index.js @@ -0,0 +1,43 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {useCallback} from 'react'; + +const KeyboardAction = ({onAction, className, children, ...rest}) => { + const handleKeyDown = useCallback(event => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + onAction(); + } + }, [onAction]); + + return ( +
+ {children} +
+ ); +}; + +export default KeyboardAction; diff --git a/hugegraph-hubble/hubble-fe/src/components/KeyboardAction/index.test.js b/hugegraph-hubble/hubble-fe/src/components/KeyboardAction/index.test.js new file mode 100644 index 000000000..c4868c447 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/KeyboardAction/index.test.js @@ -0,0 +1,50 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {useCallback, useState} from 'react'; +import {render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import KeyboardAction from './index'; + +const ExpandableAction = () => { + const [expanded, setExpanded] = useState(false); + const toggle = useCallback(() => setExpanded(value => !value), []); + + return ( + + Toggle details + + ); +}; + +test('exposes state and supports Enter and Space activation', async () => { + render(); + + const action = screen.getByRole('button', {name: 'Toggle details'}); + expect(action).toHaveAttribute('aria-expanded', 'false'); + + action.focus(); + await userEvent.keyboard('{Enter}'); + expect(action).toHaveAttribute('aria-expanded', 'true'); + + await userEvent.keyboard(' '); + expect(action).toHaveAttribute('aria-expanded', 'false'); +}); diff --git a/hugegraph-hubble/hubble-fe/src/components/ListButton/index.js b/hugegraph-hubble/hubble-fe/src/components/ListButton/index.js new file mode 100644 index 000000000..de4b89b74 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/ListButton/index.js @@ -0,0 +1,39 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {useCallback} from 'react'; +import {Button} from 'antd'; + +const GraphspaceButton = ({data, current, onClick, children}) => { + const handleClick = useCallback(() => { + onClick(data); + }, [onClick, data]); + + return ( + + ); +}; + +export default GraphspaceButton; diff --git a/hugegraph-hubble/hubble-fe/src/components/RouteErrorBoundary/index.js b/hugegraph-hubble/hubble-fe/src/components/RouteErrorBoundary/index.js new file mode 100644 index 000000000..f0cbaed0e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/RouteErrorBoundary/index.js @@ -0,0 +1,55 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import React from 'react'; +import {Button, Result} from 'antd'; + +class RouteErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = {failed: false}; + this.reload = this.reload.bind(this); + } + + static getDerivedStateFromError() { + return {failed: true}; + } + + componentDidCatch(error, info) { + console.error('Route render failed', error, info); + } + + reload() { + window.location.reload(); + } + + render() { + if (this.state.failed) { + return ( + Reload} + /> + ); + } + return this.props.children; + } +} + +export default RouteErrorBoundary; diff --git a/hugegraph-hubble/hubble-fe/src/components/RowActionButton/index.js b/hugegraph-hubble/hubble-fe/src/components/RowActionButton/index.js new file mode 100644 index 000000000..dacb65a99 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/RowActionButton/index.js @@ -0,0 +1,28 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {Button} from 'antd'; +import {useCallback} from 'react'; + +const RowActionButton = ({onAction, value, children}) => { + const handleClick = useCallback(() => onAction(value), [onAction, value]); + + return ; +}; + +export default RowActionButton; diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js new file mode 100644 index 000000000..b2e9796ee --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js @@ -0,0 +1,130 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import React, {useState} from 'react'; +import {Layout, Menu} from 'antd'; +import { + HomeOutlined, + DatabaseOutlined, + AlertOutlined, + FundViewOutlined, + MenuUnfoldOutlined, + MenuFoldOutlined, +} from '@ant-design/icons'; +import {Link, useLocation} from 'react-router-dom'; +import * as user from '../../utils/user'; +import {isPdEnabled} from '../../utils/config'; +import {getManageNavItems} from '../../utils/productMode'; +import {useTranslation} from 'react-i18next'; + +const items = t => { + const userInfo = user.getUser(); + const pdMode = isPdEnabled(); + const MY = {label: {t('home.my')}, key: 'my'}; + const ACCOUNT = {label: {t('home.account')}, key: 'account'}; + + // TODO temporary hided the resource and role modules + let systemList = [MY]; + if (!pdMode) { + systemList = [MY]; + } + else if (userInfo.is_superadmin) { + // systemList = [MY, ACCOUNT, RESOURCE, ROLE]; + systemList = [MY, ACCOUNT]; + } + else if (userInfo.resSpaces && userInfo.resSpaces.length > 0) { + // systemList = [MY, RESOURCE, ROLE]; + systemList = [MY, ACCOUNT]; + } + + // Dynamic manage menu based on deployment mode + const manageChildren = getManageNavItems(pdMode).map(item => ({ + label: {t(`manage.${item.key}`)}, + key: item.key, + })); + + const menu = [ + { + label: {t('navigation.name')}, + key: 'navigation', + icon: , + }, + { + label: t('manage.name'), + key: 'manage', + icon: , + children: manageChildren, + }, + { + label: t('analysis.name'), + key: 'analysis', + icon: , + children: [ + { + label: {t('analysis.query.name')}, + key: 'gremlin', + }, + { + label: {t('analysis.algorithm.name')}, + key: 'algorithms', + }, + { + label: {t('analysis.async_task.name')}, + key: 'asyncTasks', + }, + ], + }, + { + label: t('home.name'), + key: 'system', + icon: , + children: [...systemList], + }, + ]; + + return menu; +}; + +const Sidebar = () => { + const [collapsed, setCollapsed] = useState(false); + const href = useLocation(); + const {t} = useTranslation(); + const menuKey = href.pathname.split('/')[1] || 'navigation'; + + return ( + : + } + > + + + ); +}; + +export default Sidebar; diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.module.scss new file mode 100644 index 000000000..5cd099cf5 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.module.scss @@ -0,0 +1,21 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.trigger { + text-align: right; +} diff --git a/hugegraph-hubble/hubble-fe/src/components/SlideComponent/index.js b/hugegraph-hubble/hubble-fe/src/components/SlideComponent/index.js new file mode 100644 index 000000000..85dc3398b --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/SlideComponent/index.js @@ -0,0 +1,44 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图分析组件 slide组件 + */ + +import React from 'react'; +import {Slider} from 'antd'; +import c from './index.module.scss'; + +const SliderComponent = props => { + const {min, max, step, value, onChange} = props; + + return ( +
+ + {value || 0} +
+ ); +}; + +export default SliderComponent; diff --git a/hugegraph-hubble/hubble-fe/src/components/SlideComponent/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/SlideComponent/index.module.scss new file mode 100644 index 000000000..a5e1b2e27 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/SlideComponent/index.module.scss @@ -0,0 +1,40 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.layoutSlider { + display: flex; + align-items: center; + + :global(.ant-slider-horizontal) { + width: 90%; + } + + :global(.ant-slider-handle) { + border: solid 2px #1990ff; + }; + + :global(.ant-slider-track) { + background-color: #1990ff; + + &:hover { + border: solid 2px #1990ff; + } + } +} + + diff --git a/hugegraph-hubble/hubble-fe/src/components/Status/index.js b/hugegraph-hubble/hubble-fe/src/components/Status/index.js new file mode 100644 index 000000000..78273ba39 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Status/index.js @@ -0,0 +1,70 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {useCallback} from 'react'; +import style from './index.module.scss'; +import {useTranslation} from 'react-i18next'; + +const StatusField = ({status}) => { + const {t} = useTranslation(); + const value = status && typeof status === 'object' ? status.status : status; + const lower = value ? String(value).toLowerCase() : 'undefined'; + const config = { + new: t('common.status.new'), + running: t('common.status.running'), + success: t('common.status.success'), + cancelling: t('common.status.cancelling'), + cancelled: t('common.status.cancelled'), + failed: t('common.status.failed'), + undefined: t('common.status.undefined'), + }; + + return ( + + {config[lower] ? config[lower] : value} + + ); +}; + +const StatusA = ({onClick, disable, children}) => { + + return ( + + {children} + + ); +}; + +const StatusText = ({onClick, data, disable, children}) => { + const handleClick = useCallback(() => { + onClick(data); + }, [onClick, data]); + + return ( + disable ? ( + {children} + ) : ( + {children} + ) + ); +}; + +export {StatusField, StatusA, StatusText}; diff --git a/hugegraph-hubble/hubble-fe/src/components/Status/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/Status/index.module.scss new file mode 100644 index 000000000..35c28ecb0 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Status/index.module.scss @@ -0,0 +1,44 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.state { + border-radius: 10px; + padding: 2px 10px; + color: #2f54eb; + background-color: #d6e4ff; + font-size: 10px; + + &.new, &.cancelling, &.cancelled { + color: #ad6800; + background-color: #fff1b8; + } + + &.failed { + color: #cf1322; + background-color: #ffccc7; + } + + &.success { + color: #3f6600; + background-color: #d9f7be; + } +} + +.disable, a.disable:hover { + color: #8c8c8c; +} diff --git a/hugegraph-hubble/hubble-fe/src/components/TableHeader/index.js b/hugegraph-hubble/hubble-fe/src/components/TableHeader/index.js new file mode 100644 index 000000000..0fdf24cd8 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/TableHeader/index.js @@ -0,0 +1,29 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import style from './index.module.scss'; + +const TableHeader = ({children}) => { + return ( +
+ {children} +
+ ); +}; + +export default TableHeader; diff --git a/hugegraph-hubble/hubble-fe/src/components/TableHeader/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/TableHeader/index.module.scss new file mode 100644 index 000000000..7bea8f54e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/TableHeader/index.module.scss @@ -0,0 +1,21 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.header { + margin: 6px 0 16px; +} diff --git a/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js new file mode 100644 index 000000000..5c8670072 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js @@ -0,0 +1,132 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {Layout, Space, Avatar, Dropdown, message, Modal, Select} from 'antd'; +import {UserOutlined} from '@ant-design/icons'; +import style from './index.module.scss'; +import Logo from '../../assets/logo.png'; +import {useNavigate, useLocation} from 'react-router-dom'; +import * as api from '../../api/index'; +import * as user from '../../utils/user'; +import {useCallback, useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; + +const {Option} = Select; + +const Topbar = () => { + const userInfo = user.getUser(); + const navigate = useNavigate(); + const location = useLocation(); + const {t} = useTranslation(); + const [languageType, setLanguageType] = useState( + localStorage.getItem('languageType') || 'zh-CN' + ); + + const redirectToLogin = useCallback(() => { + const redirect = `${location.pathname}${location.search}`; + user.clearLogin(); + sessionStorage.setItem('redirect', redirect); + window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`; + }, [location.pathname, location.search]); + + useEffect(() => { + let cancelled = false; + + if (!userInfo || !userInfo.id) { + return undefined; + } + + api.auth.status() + .then(res => { + if (!cancelled && res.status === 401) { + redirectToLogin(); + } + }) + .catch(() => { + // The request interceptor has already shown the error message. + }); + + return () => { + cancelled = true; + }; + }, [redirectToLogin, userInfo]); + + if (!userInfo || !userInfo.id) { + redirectToLogin(); + } + + const i18Change = useCallback(e => { + localStorage.setItem('languageType', e); + setLanguageType(e); + window.location.reload(); + }, []); + + const logout = useCallback(() => { + + api.auth.logout().then(res => { + if (res.status === 200) { + sessionStorage.removeItem('redirect'); + user.clearLogin(); + message.success(t('Topbar.exit.success')); + navigate('/login'); + } + }); + }, [navigate, t]); + + const confirm = useCallback(() => { + Modal.confirm({ + title: t('Topbar.exit.confirm'), + okText: t('common.verify.ok'), + cancelText: t('common.verify.cancel'), + onOk: logout, + }); + }, [logout, t]); + + const userMenu = { + items: [{ + key: 'logout', + label: t('Topbar.exit.name'), + }], + onClick: confirm, + }; + + return ( + +
+
+ + + + } /> + {userInfo?.user_nickname ?? ''} + + +
+
+ ); +}; + +export default Topbar; diff --git a/hugegraph-hubble/hubble-fe/src/components/Topbar/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.module.scss new file mode 100644 index 000000000..cd18cb4d1 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.module.scss @@ -0,0 +1,51 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +div.nav { + background-color: #121212; + :global(.one-ui-pro-nav-profile) { + span, svg { + color: #fff; + } + + &:hover { + span, svg { + color: #0066ff; + } + } + } +} + +.logo { + float: left; + width: 120px; + height: 31px; +} + +.rightContainer { + display: flex; + align-items: center; + float: right; + color: #fff; + cursor: pointer; +} + +.right { + margin-left: 10px; +} + diff --git a/hugegraph-hubble/hubble-fe/src/components/Topbar/topbar-request-error.test.js b/hugegraph-hubble/hubble-fe/src/components/Topbar/topbar-request-error.test.js new file mode 100644 index 000000000..3e5aadfe5 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Topbar/topbar-request-error.test.js @@ -0,0 +1,113 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {render, waitFor} from '@testing-library/react'; +import {MemoryRouter} from 'react-router-dom'; +import Topbar from './index.ant'; +import * as api from '../../api/index'; + +jest.mock('../../api/index', () => ({ + auth: { + status: jest.fn(), + logout: jest.fn(), + }, +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: key => key, + }), +})); + +jest.mock('@ant-design/icons', () => ({ + UserOutlined: () => user icon, +})); + +jest.mock('antd', () => { + const React = require('react'); + + const Select = ({children, onChange, value}) => ( + + ); + Select.Option = ({children, value}) => ( + + ); + + return { + Layout: { + Header: ({children}) =>
{children}
, + }, + Space: ({children}) =>
{children}
, + Avatar: () => avatar, + Dropdown: ({children}) =>
{children}
, + Menu: ({items}) => ( +
+ {items?.map(item => ( + {item.label} + ))} +
+ ), + message: { + success: jest.fn(), + }, + Modal: { + confirm: jest.fn(), + }, + Select, + }; +}); + +describe('Topbar request errors', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + sessionStorage.clear(); + sessionStorage.setItem('user_', JSON.stringify({id: 'admin', user_nickname: 'admin'})); + }); + + it('catches rejected auth status checks from the request layer', async () => { + const catchHandler = jest.fn(); + api.auth.status.mockReturnValue({ + then: jest.fn(() => ({ + catch: catchHandler, + })), + }); + + render( + + + + ); + + await waitFor(() => { + expect(api.auth.status).toHaveBeenCalledTimes(1); + }); + expect(catchHandler).toHaveBeenCalledTimes(1); + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/components/common/AppBar.less b/hugegraph-hubble/hubble-fe/src/components/common/AppBar.less deleted file mode 100644 index c93dc12b6..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/common/AppBar.less +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -@active-color: #2b65ff; - -.navigator { - position: fixed; - display: flex; - width: 100%; - height: 60px; - padding: 0 16px 0 21px; - background: #121212; - box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); - z-index: 99; -} - -.navigator-logo { - width: 125px; - height: 31px; - margin: 14px 0 15px; - background: url(../../assets/imgs/logo.png); - background-size: cover; - cursor: pointer; -} - -.navigator-items { - display: flex; - margin-left: 83px; -} - -.navigator-item { - height: 60px; - color: #fff; - font-size: 16px; - cursor: pointer; - - &.dark { - color: #a1a3a6; - } - - &.active { - // border-bottom: 2px solid @active-color; - border-bottom: 2px solid #fff; - color: #fff; - // color: @active-color; - } - - & > span { - display: block; - margin: 20px auto 16px; - line-height: 24px; - } - - &:nth-child(2) { - margin-left: 60px; - } -} - -.navigator-additions { - margin-left: auto; - margin-top: 23px; - margin-bottom: 23px; - font-size: 14px; - color: #fff; - - & > span { - display: block; - } - - &.dark { - color: #505f73; - } -} - -// i18n style start -.i18n-box { - position: absolute; - top: 19px; - right: 20px -} -// i18n style end diff --git a/hugegraph-hubble/hubble-fe/src/components/common/AppBar.tsx b/hugegraph-hubble/hubble-fe/src/components/common/AppBar.tsx deleted file mode 100644 index 03168c385..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/common/AppBar.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useCallback, useState } from 'react'; -import { observer } from 'mobx-react'; -import { useLocation } from 'wouter'; -import { useTranslation } from 'react-i18next' -import { Select } from 'antd'; -import './AppBar.less'; -const { Option } = Select; - -const AppBar: React.FC = observer(() => { - const [_, setLocation] = useLocation(); - // init select language - const [languageType, setLanguageType] = useState(localStorage.getItem('languageType') || 'zh-CN') - const { t } = useTranslation() - const setRoute = useCallback( - (route: string) => () => { - setLocation(route); - }, - [setLocation] - ); - /** - * switch language and update localStorage - */ - const i18Change = (e: string) => { - localStorage.setItem('languageType', e) - setLanguageType(e) - // Refresh directly or through react.createcontext implements no refresh switching - window.location.reload() - } - return ( - - ); -}); - -export default AppBar; diff --git a/hugegraph-hubble/hubble-fe/src/components/common/LoadingDataView.less b/hugegraph-hubble/hubble-fe/src/components/common/LoadingDataView.less deleted file mode 100644 index fff4a29b2..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/common/LoadingDataView.less +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.table-data-loading { - &-wrapper { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - &-bg { - width: 144px; - height: 144px; - position: relative; - margin-bottom: 10px; - } - - &-back { - position: absolute; - left: 22px; - top: 20px; - } - - &-front { - position: absolute; - left: 57.1px; - top: 52.1px; - animation: loading-rotate 2s linear infinite; - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/common/LoadingDataView.tsx b/hugegraph-hubble/hubble-fe/src/components/common/LoadingDataView.tsx deleted file mode 100644 index 9b7c2a2c5..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/common/LoadingDataView.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import { observer } from 'mobx-react'; -import { useTranslation } from 'react-i18next'; - -import LoadingBackIcon from '../../assets/imgs/ic_loading_back.svg'; -import LoadingFrontIcon from '../../assets/imgs/ic_loading_front.svg'; - -import './LoadingDataView.less'; - -export interface LoadingDataViewProps { - isLoading: boolean; - emptyView?: React.ReactElement; -} - -const LoadingDataView: React.FC = observer( - ({ isLoading, emptyView }) => { - const { t } = useTranslation(); - - return isLoading ? ( -
-
- load background - load spinner -
- {t('common.loading-data')}... -
- ) : emptyView ? ( - emptyView - ) : null; - } -); - -export default LoadingDataView; diff --git a/hugegraph-hubble/hubble-fe/src/components/common/Tooltip.tsx b/hugegraph-hubble/hubble-fe/src/components/common/Tooltip.tsx deleted file mode 100644 index 805716425..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/common/Tooltip.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import { observer } from 'mobx-react'; -import TooltipTrigger, { Trigger } from 'react-popper-tooltip'; -import 'react-popper-tooltip/dist/styles.css'; - -import type { Placement, Modifiers } from 'popper.js'; - -export interface TooltipProps { - placement: Placement; - tooltipShown?: boolean; - trigger?: Trigger; - modifiers?: Modifiers; - tooltipWrapper: React.ReactNode; - tooltipWrapperProps?: any; - tooltipArrowClassName?: string; - childrenWrapperElement?: 'div' | 'span' | 'img'; - childrenProps?: any; - children?: React.ReactNode; -} - -const Tooltip: React.FC = observer( - ({ - placement, - tooltipShown, - trigger = 'click', - modifiers, - children, - tooltipWrapper, - tooltipWrapperProps, - tooltipArrowClassName, - childrenWrapperElement = 'span', - childrenProps - }) => ( - ( -
-
- {tooltipWrapper} -
- )} - > - {({ getTriggerProps, triggerRef }) => { - const Tag = childrenWrapperElement; - - return ( - - {children} - - ); - }} - - ) -); - -export default Tooltip; diff --git a/hugegraph-hubble/hubble-fe/src/components/common/index.ts b/hugegraph-hubble/hubble-fe/src/components/common/index.ts deleted file mode 100644 index 86e3b7dc1..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/common/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import AppBar from './AppBar'; -import Tooltip from './Tooltip'; -import LoadingDataView from './LoadingDataView'; - -export { AppBar, Tooltip, LoadingDataView }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagement.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagement.less deleted file mode 100644 index 4739cd8fc..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagement.less +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -@background-light: #fff; -@font-color-light: #000; - -@background-dark: #1b1b1b; -@font-color-grey: #6f7173; - -.graph-management { - position: absolute; - top: 60px; - left: calc(50vw - 540px); - width: 1080px; - margin: 0 auto 16px; -} - -.graph-management-header { - margin-top: 16px; - display: flex; - - &-description { - margin-right: auto; - display: block; - font-size: 14px; - line-height: 32px; - } - - &-description-community { - margin-right: auto; - - & > div:first-child { - font-size: 14px; - line-height: 18px; - } - - & > div:last-child { - font-size: 12px; - color: #999; - line-height: 17px; - } - } - - &.dark > span { - color: @font-color-grey; - } -} - -.graph-management-list { - display: flex; - width: 100%; - height: 100px; - margin: 16px 0; - background: @background-light; - border-radius: 3px; - - &:nth-last-child(2) { - margin-bottom: 24px; - } - - &.dark { - background: @background-dark; - } - - &-item { - width: 20%; - height: 100%; - padding: 24px 16px 0 16px; - - &:nth-child(1) { - width: 12.5%; - } - - &:nth-child(2) { - width: 12.5%; - } - - &:nth-child(3) { - width: 28.5%; - } - - &:nth-child(4) { - width: 8%; - } - - &:nth-child(5) { - width: 20%; - } - } - - &-data-config { - margin-top: 16px; - border-radius: 3px; - - &.dark { - background: @background-dark; - color: #fff; - } - } - - &-create-content { - display: flex; - flex-direction: column; - align-items: center; - - & > div { - margin-top: 4px; - } - - & > div > div { - margin-top: 32px; - display: flex; - justify-content: flex-end; - align-items: center; - - &:first-child { - margin-top: 0; - } - - &:nth-child(-n + 4) > span::before { - content: '*'; - color: #fb4b53; - } - - &:nth-child(-n + 2) > span { - margin-right: 4px; - } - - & > span { - display: block; - font-size: 14px; - line-height: 20px; - margin-right: 14px; - } - - & > img { - margin-right: 14px; - } - } - } - - &-title { - font-size: 12px; - line-height: 17px; - - &.dark { - color: @font-color-grey; - } - } - - &-data { - margin-top: 13px; - font-size: 16px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &.dark { - color: #fff; - } - } - - &-manipulation { - display: flex; - align-items: center; - padding: 16px; - } - - &-empty { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 1080px; - height: 580px; - margin: 16px auto; - background: @background-light; - border-radius: 3px; - font-size: 14px; - - & > div { - margin: 8px 0 24px; - } - - &.dark { - background: @background-dark; - } - } - - &-highlight { - background: transparent; - color: #2b65ff; - } -} - -.graph-data-delete-confirm { - font-size: 14px; - color: #333; - letter-spacing: 0; - line-height: 22px; -} - -// overrides - -.new-fc-one-pagination.new-fc-one-pagination-medium { - display: flex; - justify-content: center; -} - -.new-fc-one-input-error.new-fc-one-input-error-right { - position: absolute; - width: 100%; - line-height: 32px; -} - -.new-fc-one-dropdown { - z-index: 1; -} - -.new-fc-one-dropdown-menu-item { - text-align: center; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagement.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagement.tsx deleted file mode 100644 index 83c6c4d78..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagement.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; - -import { GraphManagementStoreContext } from '../../stores'; -import GraphManagementLimitHint from './GraphManagementLimitHint'; -import GraphManagementHeader from './GraphManagementHeader'; -import NewGraphConfig from './NewGraphConfig'; -import GraphManagementList from './GraphManagementList'; -import GraphManagementEmptyList from './GraphManagementEmptyList'; - -import './GraphManagement.less'; - -const GraphManagement: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - - useEffect(() => { - graphManagementStore.fetchLicenseInfo(); - graphManagementStore.fetchGraphDataList(); - - return () => { - graphManagementStore.dispose(); - }; - }, [graphManagementStore]); - - return ( -
- - - - - -
- ); -}); - -export default GraphManagement; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementEmptyList.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementEmptyList.tsx deleted file mode 100644 index fce9a4c4f..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementEmptyList.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback } from 'react'; -import { observer } from 'mobx-react'; -import { Button } from 'hubble-ui'; - -import { GraphManagementStoreContext } from '../../stores'; -import AddIcon from '../../assets/imgs/ic_add.svg'; -import EmptyIcon from '../../assets/imgs/ic_sousuo_empty.svg'; -import { useTranslation } from 'react-i18next'; - -const GraphManagementEmptyList: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const { t } = useTranslation(); - - const handleLayoutSwitch = useCallback( - (flag: boolean) => () => { - if (flag) { - graphManagementStore.fillInGraphDataDefaultConfig(); - } - graphManagementStore.switchCreateNewGraph(flag); - }, - [graphManagementStore] - ); - - if ( - graphManagementStore.graphData.length === 0 && - !graphManagementStore.showCreateNewGraph - ) { - return ( -
- {graphManagementStore.isSearched.status && - graphManagementStore.requestStatus.fetchGraphData === 'success' ? ( - <> - {t('addition.graphManagementEmptyList.no-matching-results')} -
- {t('addition.graphManagementEmptyList.no-matching-results')} -
- - ) : ( - <> - {t('addition.graphManagementEmptyList.graph-create')} -
- {t('addition.graphManagementEmptyList.graph-create-desc')} -
- - - )} -
- ); - } - - return null; -}); - -export default GraphManagementEmptyList; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementHeader.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementHeader.tsx deleted file mode 100644 index 3ba7646cf..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementHeader.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback } from 'react'; -import { observer } from 'mobx-react'; -import { Input, Button } from 'hubble-ui'; -import { GraphManagementStoreContext } from '../../stores'; -import { useTranslation } from 'react-i18next'; - -const styles = { - marginLeft: '20px', - width: 88 -}; - -const GraphManagementHeader: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const { t } = useTranslation(); - const handleLayoutSwitch = useCallback( - (flag: boolean) => () => { - if (flag) { - graphManagementStore.fillInGraphDataDefaultConfig(); - } - graphManagementStore.switchCreateNewGraph(flag); - }, - [graphManagementStore] - ); - - const handleSearchChange = useCallback( - (e: React.ChangeEvent) => { - graphManagementStore.mutateSearchWords(e.target.value); - }, - [graphManagementStore] - ); - - const handleSearch = useCallback(() => { - graphManagementStore.mutatePageNumber(1); - graphManagementStore.swtichIsSearchedStatus(true); - graphManagementStore.fetchGraphDataList(); - }, [graphManagementStore]); - - const handleClearSearch = useCallback(() => { - graphManagementStore.mutateSearchWords(''); - graphManagementStore.mutatePageNumber(1); - graphManagementStore.swtichIsSearchedStatus(false); - graphManagementStore.fetchGraphDataList(); - }, [graphManagementStore]); - - return ( -
- {graphManagementStore.licenseInfo && - graphManagementStore.licenseInfo.edition === 'community' ? ( -
-
{t('addition.graphManagementHeader.graph-manager')}
-
- {graphManagementStore.licenseInfo.edition === 'community' - ? t('addition.graphManagementHeader.community') - : t('addition.graphManagementHeader.business')} - :{t('addition.graphManagementHeader.limit-desc')}{' '} - {graphManagementStore.licenseInfo.allowed_graphs + ' '} - {t('addition.graphManagementHeader.individual')}, - {t('addition.graphManagementHeader.limit-desc1')}{' '} - {graphManagementStore.licenseInfo.allowed_datasize} -
-
- ) : ( - - {t('addition.graphManagementHeader.graph-manager')} - - )} - - -
- ); -}); - -export default GraphManagementHeader; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementLimitHint.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementLimitHint.tsx deleted file mode 100644 index e23d7530c..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementLimitHint.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Alert } from 'hubble-ui'; - -import { GraphManagementStoreContext } from '../../stores'; - -const GraphManagementLimitHint: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - let limitMessage = ''; - - for (const value of Object.values(graphManagementStore.errorInfo)) { - if (value.code === 401) { - limitMessage = value.message; - break; - } - } - - if (limitMessage === '') { - return null; - } - - return ( - - ); -}); - -export default GraphManagementLimitHint; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementList.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementList.tsx deleted file mode 100644 index 37998f499..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementList.tsx +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useCallback, - useEffect, - useState, - useLayoutEffect -} from 'react'; -import { observer } from 'mobx-react'; -import { - Embedded, - Input, - Button, - Dropdown, - Pagination, - Modal, - Message, - Tooltip -} from 'hubble-ui'; -import { useLocation } from 'wouter'; -import { isNull } from 'lodash-es'; -import { motion } from 'framer-motion'; -import Highlighter from 'react-highlight-words'; - -import { GraphManagementStoreContext } from '../../stores'; -import HintIcon from '../../assets/imgs/ic_question_mark.svg'; -import { GraphData } from '../../stores/types/GraphManagementStore/graphManagementStore'; -import { useTranslation } from 'react-i18next'; - -const commonInputProps = { - size: 'medium', - width: 420 -}; - -const listVariants = { - initial: { - opacity: 0, - y: -10 - }, - animate: { - opacity: 1, - y: 0, - transition: { - duration: 1, - ease: 'easeInOut' - } - } -}; - -const GraphManagementList: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const [width, setWidth] = useState(0); - - const handlePageChange = useCallback( - (e: React.ChangeEvent) => { - graphManagementStore.mutatePageNumber(Number(e.target.value)); - graphManagementStore.fetchGraphDataList(); - }, - [graphManagementStore] - ); - - useLayoutEffect(() => { - const inputElement = document.querySelector('.input-password input'); - - if (!isNull(inputElement)) { - inputElement.setAttribute('type', 'password'); - } - }, [graphManagementStore.showCreateNewGraph]); - - useEffect(() => { - const pg = document.querySelector( - '.new-fc-one-pagination-pager' - ) as Element; - - if (!!pg) { - const styles = getComputedStyle(pg); - const width = styles.width as string; - - setWidth(+width.split('px')[0]); - } - }, [ - graphManagementStore.graphDataPageConfig.pageTotal, - graphManagementStore.graphDataPageConfig.pageNumber - ]); - - return ( - - {graphManagementStore.graphData.map((data, index) => ( - - ))} -
- {graphManagementStore.graphDataPageConfig.pageTotal > 1 && - (graphManagementStore.showCreateNewGraph || - graphManagementStore.selectedEditIndex !== null) && ( -
- )} - -
-
- ); -}); - -const GraphManagementListItem: React.FC< - GraphData & { - index: number; - } -> = observer(({ id, name, graph, host, port, enabled, create_time, index }) => { - const { t } = useTranslation(); - const graphManagementStore = useContext(GraphManagementStoreContext); - const [_, setLocation] = useLocation(); - const [isEditing, setEditingState] = useState(false); - - const handleCancel = useCallback(() => { - graphManagementStore.resetGraphDataConfig('edit'); - graphManagementStore.changeSelectedEditIndex(null); - graphManagementStore.switchValidateStatus(false); - graphManagementStore.resetValidateErrorMessage(); - setEditingState(false); - }, [graphManagementStore]); - - const handleSave = useCallback(async () => { - graphManagementStore.switchValidateStatus(true); - - if (!graphManagementStore.validate('edit')) { - return; - } - - await graphManagementStore.upgradeGraphData(id); - - if (graphManagementStore.requestStatus.upgradeGraphData === 'success') { - Message.success({ - content: t('addition.graphManagementList.save-scuccess'), - size: 'medium', - showCloseIcon: false - }); - - graphManagementStore.fetchGraphDataList(); - handleCancel(); - } - - if (graphManagementStore.requestStatus.upgradeGraphData === 'failed') { - Message.error({ - content: graphManagementStore.errorInfo.upgradeGraphData.message, - size: 'medium', - showCloseIcon: false - }); - } - }, [graphManagementStore, handleCancel, id]); - - const handleDropdownClick = useCallback( - (index: number) => (e: { key: string; item: object }) => { - const { name } = graphManagementStore.graphData[index]; - - if (e.key === 'edit') { - setEditingState(true); - graphManagementStore.fillInGraphDataConfig(index); - graphManagementStore.changeSelectedEditIndex(index); - } - - if (e.key === 'delete') { - Modal.confirm({ - title: t('addition.graphManagementList.graph-del'), - content: ( -
- {`${t('addition.common.del-comfirm')} ${name} ${t( - 'addition.common.ask' - )}`} -
- {t('addition.graphManagementList.graph-del-comfirm-msg')} -
- ), - buttonSize: 'medium', - okText: t('addition.common.confirm'), - onOk: async () => { - await graphManagementStore.deleteGraphData(id); - - if ( - graphManagementStore.requestStatus.deleteGraphData === 'success' - ) { - Message.success({ - content: t('addition.common.del-success'), - size: 'medium', - showCloseIcon: false - }); - - graphManagementStore.fetchGraphDataList(); - } - - if ( - graphManagementStore.requestStatus.deleteGraphData === 'failed' - ) { - Message.error({ - content: graphManagementStore.errorInfo.deleteGraphData.message, - size: 'medium', - showCloseIcon: false - }); - } - } - }); - } - }, - [graphManagementStore, id] - ); - - const handleVisit = useCallback(() => { - setLocation(`/graph-management/${id}/data-analyze`); - }, [id, setLocation]); - - const dropdownList = [ - { - label: t('addition.common.edit'), - value: 'edit' - }, - { - label: t('addition.common.del'), - value: 'delete' - } - ]; - - const disabledDropdownList = [ - { - label: t('addition.common.edit'), - value: 'edit', - disabled: true - }, - { - label: t('addition.common.del'), - value: 'delete' - } - ]; - return isEditing ? ( - -
-
-
- {t('addition.graphManagementList.id')}: - - hint - - -
-
- {t('addition.graphManagementList.name')}: - - hint - - -
-
- {t('addition.graphManagementList.host')}: - -
-
- {t('addition.graphManagementList.port')}: - -
-
- {t('addition.graphManagementList.username')}: - -
-
- {t('addition.graphManagementList.password')}: - -
-
-
- - -
-
-
-
-
- ) : ( -
-
-
- {t('addition.graphManagementList.id')} -
-
- - - -
-
-
-
- {t('addition.graphManagementList.name')} -
-
- - - -
-
-
-
- {t('addition.graphManagementList.host')} -
-
- {host} -
-
-
-
- {t('addition.graphManagementList.port')} -
-
- {port} -
-
-
-
- {t('addition.graphManagementList.creation-time')} -
-
- {create_time} -
-
-
- - -
-
- ); -}); - -export default GraphManagementList; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementSidebar.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementSidebar.tsx deleted file mode 100644 index 14f7a8c93..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/GraphManagementSidebar.tsx +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useCallback, useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { useLocation, useRoute } from 'wouter'; -import { Select, Tooltip, PopLayer, Menu } from 'hubble-ui'; - -import { - GraphManagementStoreContext, - DataAnalyzeStoreContext, - ImportManagerStoreContext -} from '../../stores'; - -import ArrowIcon from '../../assets/imgs/ic_arrow_white.svg'; -import DataAnalyzeIconNormal from '../../assets/imgs/ic_shuju_normal.svg'; -import DataAnalyzeIconPressed from '../../assets/imgs/ic_shuju_pressed.svg'; -import MetaDataManagementIconNormal from '../../assets/imgs/ic_yuanshuju_normal.svg'; -import MetaDataManagementIconPressed from '../../assets/imgs/ic_yuanshuju_pressed.svg'; -import SidebarExpandIcon from '../../assets/imgs/ic_cebianzhankai.svg'; -import SidebarCollapseIcon from '../../assets/imgs/ic_cebianshouqi.svg'; -import DataImportIconNormal from '../../assets/imgs/ic_daorushuju_normal.svg'; -import DataImportIconPressed from '../../assets/imgs/ic_daorushuju_pressed.svg'; -import AsyncTaskManagerIconNormal from '../../assets/imgs/ic_renwuguanli_normal.svg'; -import AsyncTaskManagerIconPressed from '../../assets/imgs/ic_renwuguanli_pressed.svg'; -import { useTranslation } from 'react-i18next'; - -const GraphManagementSidebar: React.FC = observer(() => { - const [match, params] = useRoute( - '/graph-management/:id/:category/:subCategory?/:jobId?/:specific?/:importTasksSpecific?' - ); - - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const importManagerStore = useContext(ImportManagerStoreContext); - const [_, setLocation] = useLocation(); - // caution - const [sidebarKey, setSidebarKey] = useState(''); - const [isShowNamePop, switchShowNamePop] = useState(false); - const { t } = useTranslation(); - - const sidebarWrapperClassName = classnames({ - 'data-analyze-sidebar': true, - expand: graphManagementStore.isExpanded - }); - - const sidebarGraphSelectionClassName = classnames({ - 'data-analyze-sidebar-graph-selection': true, - expand: graphManagementStore.isExpanded - }); - - const sidebarGraphSelectionIconClassName = classnames({ - 'data-analyze-sidebar-graph-selection-icon': true, - expand: graphManagementStore.isExpanded - }); - - const sidebarMenuItemClassName = classnames({ - 'data-analyze-sidebar-menu-item': true, - expand: graphManagementStore.isExpanded - }); - - const handleOptionClick = useCallback( - (key: string) => { - setSidebarKey(key); - - switch (key) { - case 'data-analyze': - setLocation(`/graph-management/${params!.id}/data-analyze`); - return; - case 'metadata-configs': - setLocation(`/graph-management/${params!.id}/metadata-configs`); - return; - case 'data-import': - setLocation( - `/graph-management/${params!.id}/data-import/import-manager` - ); - // need to reset selectedJob here, it would change in the future - importManagerStore.setSelectedJob(null); - return; - case 'async-tasks': - setLocation(`/graph-management/${params!.id}/async-tasks`); - return; - } - }, - [params, setLocation] - ); - - const handleSelectId = useCallback( - (value: string) => { - const id = graphManagementStore.idList.find(({ name }) => name === value)! - .id; - - dataAnalyzeStore.resetIdState(); - setLocation(`/graph-management/${id}/data-analyze`); - }, - [graphManagementStore.idList, dataAnalyzeStore, setLocation] - ); - - const handleExpandClick = useCallback(() => { - graphManagementStore.switchExpanded(!graphManagementStore.isExpanded); - }, [graphManagementStore]); - - // correctly highlight sidebar option - useEffect(() => { - if (params === null) { - return; - } - - switch (params.category) { - case 'data-analyze': - setSidebarKey('data-analyze'); - return; - - case 'metadata-configs': - setSidebarKey('metadata-configs'); - return; - - case 'data-import': { - setSidebarKey('data-import'); - - return; - } - - case 'async-tasks': - setSidebarKey('async-tasks'); - return; - } - }, [params]); - - // prevent ERROR: Rendered more hooks than during the previous render - // if this block stays before any usexxx hooks, p/n render hooks is not equal - if ( - !match || - params?.subCategory === 'job-error-log' || - params?.jobId === 'task-error-log' || - (params?.category === 'async-tasks' && params?.jobId === 'result') - ) { - return null; - } - - return ( -
    -
  • - {!graphManagementStore.isExpanded ? ( - - } - visible={isShowNamePop} - > -
    { - switchShowNamePop(!isShowNamePop); - }} - > -
    G
    -
    - {t('addition.graphManagementSidebar.graph-select')} -
    -
    -
    - ) : ( - <> -
    G
    -
    - -
    - - )} -
  • -
    - { - handleOptionClick(e.key); - }} - style={{ - width: graphManagementStore.isExpanded ? 200 : 60 - }} - > - -
    - {t('addition.graphManagementSidebar.data-analysis')} -
    {t('addition.graphManagementSidebar.data-analysis')}
    -
    -
    - -
    - {t('addition.graphManagementSidebar.metadata-config')} -
    {t('addition.graphManagementSidebar.metadata-config')}
    -
    -
    - -
    - {t('addition.graphManagementSidebar.data-import')} -
    {t('addition.graphManagementSidebar.data-import')}
    -
    -
    - {/* - 数据导入 -
    数据导入
    -
    - } - > - -
    导入任务
    -
    - */} - -
    - {t('addition.graphManagementSidebar.task-management')} -
    {t('addition.graphManagementSidebar.task-management')}
    -
    -
    -
-
-
  • - {graphManagementStore.isExpanded ? ( - {t('addition.common.fold')} - ) : ( - {t('addition.common.open')} - )} -
  • - - ); -}); - -export interface GraphSelectMenuProps { - routeId: number; - isShowNamePop: boolean; - switchShowPop: (flag: boolean) => void; -} - -const GraphSelectMenu: React.FC = observer( - ({ routeId, switchShowPop }) => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const [_, setLocation] = useLocation(); - - const handleSelectDropdownId = useCallback( - (id) => (e: any) => { - const targetClassName = (e.target as Element).className; - - if ( - targetClassName !== - 'data-analyze-sidebar-dropdown-menu-item-wrapper disabled' && - targetClassName !== 'data-analyze-sidebar-dropdown-menu-item disabled' - ) { - switchShowPop(false); - dataAnalyzeStore.resetIdState(); - setLocation(`/graph-management/${id}/data-analyze`); - } - }, - [dataAnalyzeStore, setLocation, switchShowPop] - ); - - useEffect(() => { - const cb = (e: MouseEvent) => { - if ( - (e.target as Element).className !== - 'data-analyze-sidebar-graph-selection-icon' && - (e.target as Element).className !== - 'data-analyze-sidebar-graph-selection-instruction' && - (e.target as Element).nodeName.toLowerCase() !== 'img' && - (e.target as Element).className !== - 'data-analyze-sidebar-dropdown-menu-item-wrapper disabled' && - (e.target as Element).className !== - 'data-analyze-sidebar-dropdown-menu-item disabled' - ) { - switchShowPop(false); - } - }; - - window.addEventListener('click', cb, true); - - return () => { - window.removeEventListener('click', cb); - }; - }, [switchShowPop]); - - return ( -
    -
    - {graphManagementStore.idList.map(({ id, name }) => { - const dropdownItemWrapperClassName = classnames({ - 'data-analyze-sidebar-dropdown-menu-item-wrapper': true, - disabled: routeId === id - }); - - const dropdownItemClassName = classnames({ - 'data-analyze-sidebar-dropdown-menu-item': true, - disabled: routeId === id - }); - - return ( -
    -
    {name}
    -
    - ); - })} -
    -
    - ); - } -); - -export default GraphManagementSidebar; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/NewGraphConfig.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/NewGraphConfig.tsx deleted file mode 100644 index d7d2df68d..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/NewGraphConfig.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback, useLayoutEffect } from 'react'; -import { observer } from 'mobx-react'; -import { Embedded, Input, Button, Message, Tooltip } from 'hubble-ui'; - -import { GraphManagementStoreContext } from '../../stores'; -import HintIcon from '../../assets/imgs/ic_question_mark.svg'; -import { isNull } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; - -const commonInputProps = { - size: 'medium', - width: 420 -}; - -const NewGraphConfig: React.FC = observer(() => { - const { t } = useTranslation(); - const isRequiredInputProps = { - ...commonInputProps, - isRequired: true, - requiredErrorMessage: t('addition.common.required') - }; - const graphManagementStore = useContext(GraphManagementStoreContext); - - const handleCancel = useCallback(() => { - graphManagementStore.switchCreateNewGraph(false); - graphManagementStore.resetGraphDataConfig('new'); - graphManagementStore.switchValidateStatus(false); - graphManagementStore.resetValidateErrorMessage(); - }, [graphManagementStore]); - - const handleCreate = useCallback(async () => { - graphManagementStore.switchValidateStatus(true); - - if (!graphManagementStore.validate('new')) { - return; - } - - await graphManagementStore.AddGraphData(); - - if (graphManagementStore.requestStatus.AddGraphData === 'success') { - Message.success({ - content: t('addition.newGraphConfig.create-success'), - size: 'medium', - showCloseIcon: false - }); - - graphManagementStore.fetchGraphDataList(); - handleCancel(); - } - - if (graphManagementStore.requestStatus.AddGraphData === 'failed') { - Message.error({ - content: graphManagementStore.errorInfo.AddGraphData.message, - size: 'medium', - showCloseIcon: false - }); - } - }, [graphManagementStore, handleCancel]); - - useLayoutEffect(() => { - const inputElement = document.querySelector('.input-password input'); - - if (!isNull(inputElement)) { - inputElement.setAttribute('type', 'password'); - } - }, [graphManagementStore.showCreateNewGraph]); - - return ( - -
    -
    -
    - {t('addition.newGraphConfig.id')}: - - hint - - -
    -
    - {t('addition.newGraphConfig.name')}: - - hint - - -
    -
    - {t('addition.newGraphConfig.host')}: - -
    -
    - {t('addition.newGraphConfig.port')}: - -
    -
    - {t('addition.newGraphConfig.username')}: - -
    -
    - {t('addition.newGraphConfig.password')}: - -
    -
    -
    - - -
    -
    -
    -
    -
    - ); -}); - -export default NewGraphConfig; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskList.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskList.less deleted file mode 100644 index d1791a074..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskList.less +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.async-task-list { - position: absolute; - width: calc(100% - 60px); - padding: 0 16px 16px; - left: 60px; - top: 60px; - - &-with-expand-sidebar { - width: calc(100% - 200px); - left: 200px; - } - - &-breadcrumb-wrapper { - margin: 16px 0; - line-height: 22px; - } - - &-content-wrapper { - height: calc(100vh - 130px); - background: #fff; - padding: 16px; - overflow: auto; - - // override - & table { - table-layout: fixed; - } - } - - &-content-header { - margin-bottom: 16px; - display: flex; - justify-content: flex-end; - } - - &-table-selected-reveals { - width: 100%; - height: 40px; - padding: 0 16px; - display: flex; - background: #2b65ff; - border-radius: 3px 3px 0 0; - font-size: 14px; - align-items: center; - color: #fff; - - & > div { - margin-right: 12px; - } - - & > img { - margin-left: auto; - cursor: pointer; - } - } - - &-table-status-wrapper { - width: 58px; - line-height: 22px; - text-align: center; - - &.scheduling, - &.scheduled, - &.queued, - &.restoring, - &.running { - background-color: #f2f7ff; - border: 1px solid #8cb8ff; - color: #3d88f2; - } - - &.cancelled { - border: 1px solid #e0e0e0; - background-color: #f5f5f5; - color: #333; - } - - &.success { - width: 44px; - border: 1px solid #7ed988; - background-color: #f2fff4; - color: #39bf45; - } - - &.failed { - width: 44px; - border: 1px solid #ff9499; - background-color: #fff2f2; - color: #e64552; - } - } - - &-table-manipulations { - display: flex; - justify-content: flex-end; - color: #2b65ff; - width: 100px; - - & > span { - cursor: pointer; - - &:hover { - color: #527dff; - } - - &:active { - color: #184bcc; - } - - &:last-child { - margin-left: 16px; - } - } - } - - &-table-filters-wrapper { - background: #fff; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - padding: 4px 0; - - & > div { - width: 120px; - padding: 5px 16px; - line-height: 22px; - font-size: 14px; - color: #333; - cursor: pointer; - - &:hover { - background-color: #f2f7ff; - } - } - - &.type- > div, - &.status- > div { - &:nth-child(1) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - - &.type-gremlin > div, - &.status-new > div { - &:nth-child(2) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - - &.type-algorithm > div, - &.status-queued > div { - &:nth-child(3) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - - &.type-remove_schema > div, - &.status-running > div { - &:nth-child(4) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - - &.type-create_index > div, - &.status-restoring > div { - &:nth-child(5) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - - &.type-rebuild_index > div, - &.status-success > div { - &:nth-child(6) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - - &.status-failed > div { - &:nth-child(7) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - - &.status-cancelled > div { - &:nth-child(8) { - background-color: #f2f7ff; - color: #2b65ff; - font-weight: 700; - } - } - } - - &-table-outlink { - font-size: 14px; - color: #2b65ff; - line-height: 22px; - cursor: pointer; - display: flex; - width: fit-content; - text-decoration: none; - - &:hover { - color: #527dff; - } - - &:active { - color: #184bcc; - } - - &-disabled { - color: #999; - } - } - - &-tooltip { - padding: 24px; - background-color: #fff; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - font-size: 14px; - color: #333; - z-index: 99; - - &-title { - font-weight: 900; - margin-bottom: 16px; - } - - &-description { - margin-bottom: 24px; - } - } - - &-error-layout { - width: 228px; - margin: 3px 0 8px; - line-height: 22px; - - &-title { - font-weight: 700; - margin-bottom: 4px; - } - - & > p { - cursor: pointer; - color: #2b65ff; - } - - &-expand > p { - cursor: default; - color: #000; - word-wrap: break-word; - } - } -} - -/* override */ - -.async-task-list .new-fc-one-table { - border: 0; -} - -// .async-task-list .new-fc-one-table-thead > tr > th:first-of-type { -// padding: 12px 16px 12px 20px; -// } diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskList.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskList.tsx deleted file mode 100644 index 740b750d3..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskList.tsx +++ /dev/null @@ -1,855 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useCallback, - useEffect, - useState, - useRef -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { useRoute } from 'wouter'; -import { useTranslation } from 'react-i18next'; -import { isEmpty, size, intersection, without } from 'lodash-es'; -import { Breadcrumb, Input, Button, Message, Table, Loading } from 'hubble-ui'; - -import { - GraphManagementStoreContext, - AsyncTasksStoreContext -} from '../../../stores'; -import { LoadingDataView, Tooltip } from '../../common'; -import AddIcon from '../../../assets/imgs/ic_add.svg'; -import WhiteCloseIcon from '../../../assets/imgs/ic_close_white.svg'; - -import type { AsyncTask } from '../../../stores/types/GraphManagementStore/asyncTasksStore'; - -import './AsyncTaskList.less'; -import { isUndefined } from 'util'; - -const AsyncTaskList: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const asyncTasksStore = useContext(AsyncTasksStoreContext); - const [, params] = useRoute('/graph-management/:id/async-tasks'); - const [selectedRowKeys, mutateSelectedRowKeys] = useState([]); - const [isShowTypeFilter, switchShowTypeFilter] = useState(false); - const [isShowStatusFilter, switchShowStatusFilter] = useState(false); - const [isShowBatchDeleteModal, switchShowBatchDeleteModal] = useState(false); - const [preLoading, switchPreLoading] = useState(true); - const [isInLoop, switchInLoop] = useState(false); - - const deleteWrapperRef = useRef(null); - - const { t } = useTranslation(); - - const currentSelectedRowKeys = intersection( - selectedRowKeys, - asyncTasksStore.asyncTaskList.map(({ id }) => id) - ); - - const handleSelectedTableRow = (newSelectedRowKeys: number[]) => { - mutateSelectedRowKeys(newSelectedRowKeys); - }; - - const handleSearchChange = (e: React.ChangeEvent) => { - asyncTasksStore.mutateSearchWords(e.target.value); - }; - - const handleSearch = () => { - asyncTasksStore.mutateAsyncTasksPageNumber(1); - asyncTasksStore.switchSearchedStatus(true); - asyncTasksStore.fetchAsyncTaskList(); - }; - - const handlePageChange = (e: React.ChangeEvent) => { - switchPreLoading(true); - - asyncTasksStore.mutateAsyncTasksPageNumber(Number(e.target.value)); - asyncTasksStore.fetchAsyncTaskList(); - }; - - const handleClearSearch = () => { - asyncTasksStore.mutateSearchWords(''); - asyncTasksStore.mutateAsyncTasksPageNumber(1); - asyncTasksStore.switchSearchedStatus(false); - asyncTasksStore.fetchAsyncTaskList(); - }; - - const handleFilterOptions = ( - categroy: 'type' | 'status', - option: string - ) => () => { - asyncTasksStore.mutateFilterOptions(categroy, option); - // reset page number to beginning - asyncTasksStore.mutateAsyncTasksPageNumber(1); - asyncTasksStore.fetchAsyncTaskList(); - - categroy === 'type' - ? switchShowTypeFilter(false) - : switchShowStatusFilter(false); - }; - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isShowBatchDeleteModal !== null && - deleteWrapperRef && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchShowBatchDeleteModal(false); - } - }, - [deleteWrapperRef, isShowBatchDeleteModal] - ); - - const columnConfigs = [ - { - title: t('async-tasks.table-column-title.task-id'), - dataIndex: 'id', - render(text: string) { - return ( -
    - {text} -
    - ); - } - }, - { - title: t('async-tasks.table-column-title.task-name'), - dataIndex: 'task_name', - width: '20%', - render(text: string) { - return ( -
    - {text} -
    - ); - } - }, - { - title: t('async-tasks.table-column-title.task-type'), - dataIndex: 'task_type', - width: '13%', - onFilterDropdownVisibleChange(visible: boolean) { - switchShowTypeFilter(visible); - }, - filterDropdownVisible: isShowTypeFilter, - filterDropdown: ( -
    -
    - {t('async-tasks.table-filters.task-type.all')} -
    -
    - {t('async-tasks.table-filters.task-type.gremlin')} -
    -
    - {t('async-tasks.table-filters.task-type.algorithm')} -
    -
    - {t('async-tasks.table-filters.task-type.remove-schema')} -
    -
    - {t('async-tasks.table-filters.task-type.create-index')} -
    -
    - {t('async-tasks.table-filters.task-type.rebuild-index')} -
    -
    - ), - render(text: string) { - return ( -
    - {t(`async-tasks.table-filters.task-type.${text.replace('_', '-')}`)} -
    - ); - } - }, - { - title: t('async-tasks.table-column-title.create-time'), - dataIndex: 'task_create', - width: '18%', - render(timeStamp: string) { - const date = new Date(timeStamp); - const convertedDate = `${date.toISOString().split('T')[0]} ${ - date.toTimeString().split(' ')[0] - }`; - - return ( -
    - {convertedDate} -
    - ); - } - }, - { - title: t('async-tasks.table-column-title.time-consuming'), - width: '12%', - render(rowData: AsyncTask) { - const duration = - Number(rowData.task_update) - Number(rowData.task_create); - let restTime = duration; - let timeString = ''; - - if (Math.floor(duration / 1000 / 60 / 60 / 24) > 0) { - const dayDiffernce = Math.floor(duration / 1000 / 60 / 60 / 24); - timeString += dayDiffernce + 'd'; - restTime -= dayDiffernce * 1000 * 60 * 60 * 24; - } - - if (Math.floor(restTime / 1000 / 60 / 60) > 0) { - const dayDiffernce = Math.floor(restTime / 1000 / 60 / 60); - timeString += ' ' + dayDiffernce + 'h'; - restTime -= dayDiffernce * 1000 * 60 * 60; - } - - if (Math.floor(restTime / 1000 / 60) > 0) { - const dayDiffernce = Math.floor(restTime / 1000 / 60); - timeString += ' ' + dayDiffernce + 'm'; - restTime -= dayDiffernce * 1000 * 60; - } - - if (Math.floor(restTime / 1000) > 0) { - const dayDiffernce = Math.floor(restTime / 1000); - timeString += ' ' + dayDiffernce + 's'; - restTime -= dayDiffernce * 1000; - } - - if (restTime > 0) { - timeString += ' ' + restTime + 'ms'; - } - - if (restTime <= 0) { - timeString = '0s'; - } - - return
    {timeString}
    ; - } - }, - { - title: t('async-tasks.table-column-title.status'), - dataIndex: 'task_status', - onFilterDropdownVisibleChange(visible: boolean) { - switchShowStatusFilter(visible); - }, - filterDropdownVisible: isShowStatusFilter, - filterDropdown: ( -
    -
    - {t('async-tasks.table-filters.status.all')} -
    -
    - {t('async-tasks.table-filters.status.scheduling')} -
    -
    - {t('async-tasks.table-filters.status.queued')} -
    -
    - {t('async-tasks.table-filters.status.running')} -
    -
    - {t('async-tasks.table-filters.status.restoring')} -
    -
    - {t('async-tasks.table-filters.status.success')} -
    -
    - {t('async-tasks.table-filters.status.failed')} -
    -
    - {t('async-tasks.table-filters.status.cancelled')} -
    -
    - ), - width: '8%', - render(text: string) { - return ( -
    - {t(`async-tasks.table-filters.status.${text}`)} -
    - ); - } - }, - { - title: t('async-tasks.table-column-title.manipulation'), - width: '15%', - render(rowData: AsyncTask) { - return ( -
    - -
    - ); - } - } - ]; - - const isLoading = - preLoading || - asyncTasksStore.requestStatus.fetchAsyncTaskList === 'pending'; - - const wrapperClassName = classnames({ - 'async-task-list': true, - 'async-task-list-with-expand-sidebar': graphManagementStore.isExpanded - }); - - useEffect(() => { - if (params !== null) { - graphManagementStore.fetchIdList(); - asyncTasksStore.setCurrentId(Number(params!.id)); - let startTime = new Date().getTime(); - let timerId: number; - - const loopFetchAsyncTaskList = async () => { - const currentTime = new Date().getTime(); - - if (currentTime - startTime > 1000 * 60 * 10) { - switchInLoop(false); - return; - } - - await asyncTasksStore.fetchAsyncTaskList(); - - if (asyncTasksStore.requestStatus.fetchAsyncTaskList === 'failed') { - Message.error({ - content: asyncTasksStore.errorInfo.fetchAsyncTaskList.message, - size: 'medium', - showCloseIcon: false - }); - - return; - } - - if ( - !isUndefined( - asyncTasksStore.asyncTaskList.find( - ({ task_status }) => - task_status === 'scheduling' || - task_status === 'scheduled' || - task_status === 'queued' || - task_status === 'running' || - task_status === 'restoring' - ) - ) - ) { - timerId = window.setTimeout(() => { - switchInLoop(true); - loopFetchAsyncTaskList(); - }, 5000); - } else { - switchInLoop(false); - return; - } - }; - - loopFetchAsyncTaskList(); - - return () => { - switchInLoop(false); - window.clearTimeout(timerId); - }; - } - // when page number changed, dispatch current useEffect to loop again - }, [params?.id, asyncTasksStore.asyncTasksPageConfig.pageNumber]); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - useEffect(() => { - setTimeout(() => { - switchPreLoading(false); - }, 800); - }, [asyncTasksStore.asyncTasksPageConfig.pageNumber]); - - // dynamic edit table border-radius when selected row changes - useEffect(() => { - const tableWrapperRef = document.querySelector('.new-fc-one-table'); - const tableRef = document.querySelector('.new-fc-one-table table'); - - if (tableRef !== null) { - if (size(selectedRowKeys) !== 0) { - (tableWrapperRef as HTMLDivElement).style.borderRadius = '0'; - (tableRef as HTMLTableElement).style.borderRadius = '0'; - } else { - (tableWrapperRef as HTMLDivElement).style.borderRadius = '2px 2px 0 0'; - (tableRef as HTMLTableElement).style.borderRadius = '2px 2px 0 0'; - } - } - }, [selectedRowKeys]); - - useEffect(() => { - return () => { - asyncTasksStore.dispose(); - }; - }, []); - - return ( -
    -
    - - {t('async-tasks.title')} - -
    -
    -
    - -
    - {size(currentSelectedRowKeys) !== 0 && ( -
    -
    - {t('async-tasks.table-selection.selected', { - number: size(currentSelectedRowKeys) - })} -
    - - <> -

    - {t('async-tasks.hint.delete-batch-confirm')} -

    -

    - {t('async-tasks.hint.delete-batch-description')} -

    -
    - - -
    - -
    - } - childrenWrapperElement="div" - > - - - close { - mutateSelectedRowKeys([]); - }} - /> -
    - )} - rowData.id} - rowSelection={{ - selectedRowKeys, - onChange: handleSelectedTableRow, - getCheckboxProps(records: AsyncTask) { - const { task_status } = records; - return { - disabled: - task_status === 'scheduling' || - task_status === 'scheduled' || - task_status === 'queued' || - task_status === 'running' || - task_status === 'restoring' || - (!isEmpty(selectedRowKeys) && - asyncTasksStore.requestStatus.deleteAsyncTask === 'pending') - }; - } - }} - locale={{ - emptyText: ( - {t('async-tasks.hint.no-data')} - ) : ( - - ) - } - /> - ) - }} - dataSource={ - isLoading && !isInLoop ? [] : asyncTasksStore.asyncTaskList - } - pagination={{ - hideOnSinglePage: false, - pageNo: asyncTasksStore.asyncTasksPageConfig.pageNumber, - pageSize: 10, - showSizeChange: false, - showPageJumper: false, - total: asyncTasksStore.asyncTasksPageConfig.pageTotal, - onPageNoChange: handlePageChange - }} - /> - - - ); -}); - -export interface AsyncTaskListManipulationProps { - id: number; - type: string; - status: string; -} - -export const AsyncTaskListManipulation: React.FC = observer( - ({ id, type, status }) => { - const asyncTasksStore = useContext(AsyncTasksStoreContext); - const [isPopDeleteModal, switchPopDeleteModal] = useState(false); - const deleteWrapperRef = useRef(null); - const [, params] = useRoute('/graph-management/:id/async-tasks'); - const { t } = useTranslation(); - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isPopDeleteModal !== null && - deleteWrapperRef && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchPopDeleteModal(false); - } - }, - [deleteWrapperRef, isPopDeleteModal] - ); - - // some status only has one manipulation, if all of them in such status - // no margin left needed (also flex-end) - const shouldLeftMargin = !isEmpty( - asyncTasksStore.asyncTaskList.filter( - ({ task_status, task_type }) => - task_status === 'failed' || - (task_status === 'success' && - (task_type === 'gremlin' || task_type === 'algorithm')) - ) - ); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( -
    - {status === 'success' && (type === 'gremlin' || type === 'algorithm') && ( - - {t('async-tasks.manipulations.check-result')} - - )} - {status === 'failed' && ( - - {t('async-tasks.manipulations.check-reason')} - - )} - {status !== 'scheduling' && - status !== 'scheduled' && - status !== 'queued' && - status !== 'running' && - status !== 'restoring' && ( - -

    - {t('async-tasks.hint.delete-confirm')} -

    -

    - {t('async-tasks.hint.delete-description')} -

    -
    - - -
    -
    - } - childrenProps={{ - style: { - marginLeft: shouldLeftMargin ? '16px' : 0 - }, - onClick() { - switchPopDeleteModal(true); - } - }} - > - {t('async-tasks.manipulations.delete')} - - )} - {(status === 'scheduling' || - status === 'scheduled' || - status === 'queued' || - status === 'running' || - status === 'restoring') && ( - { - await asyncTasksStore.abortAsyncTask(id); - asyncTasksStore.fetchAsyncTaskList(); - }} - style={{ marginLeft: shouldLeftMargin ? '16px' : 0 }} - > - {t('async-tasks.manipulations.abort')} - - )} - {status === 'cancelling' && ( -
    - - - {t('async-tasks.manipulations.aborting')} - -
    - )} - - ); - } -); - -export const EmptyAsyncTaskHints: React.FC = observer(() => { - const { t } = useTranslation(); - - return ( -
    - {t('async-tasks.hint.empty')} -
    {t('async-tasks.hint.empty')}
    -
    - ); -}); - -export const AsyncTaskDeleteErrorLayout: React.FC = observer(() => { - const asyncTasksStore = useContext(AsyncTasksStoreContext); - const { t } = useTranslation(); - const [isShowDetails, switchShowDetails] = useState(false); - - const errorLayoutWrapper = classnames({ - 'async-task-list-error-layout': true, - 'async-task-list-error-layout-expand': isShowDetails - }); - - return ( -
    -
    - {t('async-tasks.hint.creation-failed')} -
    - {isShowDetails ? ( -

    {asyncTasksStore.errorInfo.deleteAsyncTask.message}

    - ) : ( -

    { - switchShowDetails(true); - }} - > - {t('async-tasks.hint.check-details')} -

    - )} -
    - ); -}); - -export default AsyncTaskList; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskResult.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskResult.less deleted file mode 100644 index f3e387958..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskResult.less +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.async-task-result { - width: 75vw; - position: relative; - top: 76px; - margin: 0 auto 16px; - padding: 16px; - font-size: 14px; - background-color: #fff; - word-wrap: break-word; - - &-error { - line-height: 2; - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskResult.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskResult.tsx deleted file mode 100644 index 305b8e212..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/AsyncTaskResult.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect } from 'react'; -import { useRoute } from 'wouter'; -import { observer } from 'mobx-react'; -import { isNull } from 'lodash-es'; -import ReactJsonView from 'react-json-view'; - -import { convertStringToJSON } from '../../../utils'; -import { AsyncTasksStoreContext } from '../../../stores'; - -import './AsyncTaskResult.less'; - -const TaskErrorLogs: React.FC = observer(() => { - const asyncTasksStore = useContext(AsyncTasksStoreContext); - const [, params] = useRoute( - '/graph-management/:id/async-tasks/:taskId/result' - ); - const taskResult = isNull(asyncTasksStore.singleAsyncTask) - ? null - : convertStringToJSON(asyncTasksStore.singleAsyncTask!.task_result); - - useEffect(() => { - asyncTasksStore.setCurrentId(Number(params!.id)); - asyncTasksStore.fetchAsyncTask(Number(params!.taskId)); - - return () => { - asyncTasksStore.dispose(); - }; - }, [params!.id, params!.taskId]); - - return ( -
    - {!isNull(asyncTasksStore.singleAsyncTask) && - (asyncTasksStore.singleAsyncTask.task_status === 'success' ? ( - !isNull(taskResult) ? ( - - ) : ( - asyncTasksStore.singleAsyncTask!.task_result - ) - ) : ( -
    - {asyncTasksStore.singleAsyncTask!.task_result} -
    - ))} -
    - ); -}); - -export default TaskErrorLogs; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/index.ts deleted file mode 100644 index a0cbff8f3..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/async-tasks/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import AsyncTaskList from './AsyncTaskList'; - -export { AsyncTaskList }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyze.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyze.less deleted file mode 100644 index 3d7ec2ecd..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyze.less +++ /dev/null @@ -1,1230 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.data-analyze { - position: relative; - - &-sidebar { - width: 60px; - height: calc(100vh - 60px); - position: fixed; - top: 60px; - background: #fff; - display: flex; - flex-direction: column; - - & > li { - display: flex; - justify-content: center; - align-items: center; - } - - &.expand { - width: 200px; - - & > li { - justify-content: flex-start; - padding: 16px; - } - - & > li:nth-of-type(1) { - padding-left: 20px; - } - - & > li:nth-last-of-type(1) { - justify-content: center; - } - } - - &-menu-item { - display: flex; - align-items: center; - // make it center - padding-left: 2px; - - & > div { - margin-left: 16px; - } - - &.expand { - padding-left: 0; - - & > div { - margin-left: 7.8px; - } - } - } - - &-graph-selection { - flex-direction: column; - margin-top: 8.5px; - height: 51.3px; - - &.expand { - flex-direction: row; - padding-left: 20px; - } - - & > div { - width: 24px; - cursor: pointer; - } - - &-icon { - height: 24px; - line-height: 24px; - background: #2b65ff; - text-align: center; - font-size: 14px; - color: #fff; - border-radius: 3px 3px 0 0; - - &.expand { - border-radius: 3px; - } - } - - &-instruction { - height: 12px; - border-radius: 0 0 3px 3px; - opacity: 0.8; - background: #2b65ff; - padding-top: 2.5px; - - & > img { - display: block; - margin: 0 auto; - } - } - } - - &-expand-control { - height: 48px; - margin-top: auto; - cursor: pointer; - - &:hover { - background: #f5f5f5; - } - } - - &-dropdown-menu { - width: 168px; - max-height: 342px; - overflow: auto; - padding: 4px 0; - background: #fff; - border-radius: 3px; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - - &-item-wrapper { - width: 100%; - padding: 0 16px; - - &:hover { - background: #f5f5f5; - } - - &.disabled { - cursor: not-allowed; - - &:hover { - background: #fff; - } - } - } - - &-item { - font-size: 14px; - color: #333; - line-height: 32px; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &.disabled { - color: #ccc; - cursor: not-allowed; - - &:hover { - background: #fff; - } - } - } - } - } - - &-content { - position: absolute; - width: calc(100% - 60px); - padding: 0 16px 16px 16px; - left: 60px; - top: 60px; - - &.sidebar-expand { - width: calc(100% - 200px); - left: 200px; - } - } - - .query-tab-index-wrapper { - display: flex; - width: 100%; - margin-top: 16px; - - & > .query-tab-index { - padding: 10px 20px; - height: 40px; - color: #333; - font-size: 14px; - text-align: center; - line-height: 20px; - cursor: pointer; - -moz-user-select: none; /* ff */ - -webkit-user-select: none; /* webkit */ - -ms-user-select: none; /* IE10 */ - -khtml-user-select: none; /* obsolete */ - user-select: none; - - &.active { - background-color: #fff; - color: #2b65ff; - border-radius: 3px 3px 0 0; - } - } - } - - .query-tab-algorithm-wrapper, - .query-tab-content-wrapper { - padding: 16px; - background: #fff; - border-radius: 3px 3px 3px 3px; - } - - .query-tab-algorithm-wrapper { - // min-width: 1220px; - overflow: auto; - - .new-fc-one-input-error.new-fc-one-input-error-right { - left: 0; - top: 30px; - } - } - - .query-tab-algorithm-hint { - width: fit-content; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - line-height: 18px; - padding: 16px; - color: #e64552; - font-size: 14px; - text-align: center; - } - - .query-tab-content { - display: flex; - } - - // v1.6.0 algorithm analyze - .query-tab-content-title { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 14px; - font-weight: 900; - line-height: 24px; - margin-bottom: 14px; - - & img { - cursor: pointer; - } - } - - .query-tab-content-menu { - display: flex; - align-items: center; - font-size: 14px; - color: #333; - line-height: 32px; - margin-bottom: 14px; - - &:last-child { - margin-bottom: 0; - } - - & > span { - width: 200px; - padding-left: 20px; - - &:hover { - color: #2b65ff; - background-color: #f2f7ff; - cursor: pointer; - } - } - } - - .query-tab-content-form { - padding: 0 32px 0 0; - } - - .query-tab-content-form-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 32px; - - &:last-child { - margin-bottom: 0; - } - } - - .query-tab-content-form-item { - display: flex; - align-items: center; - } - - .query-tab-content-form-item-title { - display: flex; - justify-content: flex-end; - align-items: center; - font-size: 14px; - margin-right: 32px; - min-width: 95px; - max-width: 120px; - line-height: 22px; - white-space: nowrap; - - & > i { - color: #fb4b53; - } - - &.large { - min-width: 140px; - max-width: max-content; - } - } - - .query-tab-content-form-expand-title { - min-width: 150px; - margin-right: 32px; - font-size: 14px; - line-height: 22px; - } - - .query-tab-content-form-expand-items { - background-color: #f5f5f5; - padding: 16px 8px; - margin-bottom: 16px; - - &:last-child { - margin-bottom: 0; - } - } - - .query-tab-content-form-expand-item { - display: flex; - align-items: center; - margin-bottom: 32px; - - &:last-child { - margin-bottom: 0; - } - } - - .query-tab-content-internal-expand-row { - width: 390px; - display: flex; - // justify-content: space-between; - height: 32px; - margin-bottom: 16px; - } - - .query-tab-content-internal-expand-input { - margin: 0 14px 0 16px; - } - - .query-tab-content-internal-expand-manipulation { - &-enable, - &-disabled { - font-size: 14px; - line-height: 20px; - margin-bottom: 32px; - padding-left: 172px; - - & span { - cursor: pointer; - } - } - - &-enable { - color: #2b65ff; - } - - &-disabled { - color: #999; - } - } - - .query-tab-code-edit { - width: calc(100% - 55px); - - &.hide { - display: none; - } - } - - .query-tab-code-editor { - display: none; - } - - .query-tab-favorite { - display: flex; - flex-direction: column; - - & > span { - display: block; - line-height: 22px; - margin-bottom: 16px; - font-family: - 'PingFangSC-Medium', - 'Microsoft YaHei', - '微软雅黑', - Arial, - sans-serif; - font-weight: bold; - color: #000; - letter-spacing: 0; - } - - &-footer { - width: 100%; - margin-top: 24px; - display: flex; - } - } - - .query-tab-expand { - width: 55px; - height: 30px; - display: flex; - justify-content: space-around; - align-items: center; - font-size: 14px; - cursor: pointer; - - & > img { - transform: rotate(180deg); - margin-right: 8px; - } - } - - .query-tab-collpase { - .query-tab-expand(); - - width: 100%; - cursor: auto; - justify-content: flex-end; - - & > div { - display: flex; - align-items: center; - cursor: pointer; - - & > img { - margin-right: 8px; - } - } - } - - .query-tab-manipulations { - display: flex; - margin-top: 16px; - background: #fff; - border-radius: 0 0 3px 3px; - } - - .query-choose-type-button { - display: flex; - justify-content: center; - align-items: center; - width: 26px; - border-left: 1px solid #8cb8ff; - border-radius: 0 2px 2px 0; - background-color: #2b65ff; - margin-right: 12px; - cursor: pointer; - - &-disabled { - background-color: #d9e6ff; - cursor: not-allowed; - } - } - - .query-result { - display: flex; - height: calc(100vh - 312px); - margin-top: 24px; - background: #fff; - border-radius: 3px; - - &.query-result-fullscreen { - position: fixed; - top: 0; - left: 0; - z-index: 99; - margin-top: 0; - width: 100vw; - height: 100vh; - border-radius: 0; - } - - &-sidebar { - width: 61px; - height: 100%; - border-right: 1px solid #e0e0e0; - display: flex; - flex-direction: column; - overflow-y: auto; - - &-options { - width: 61px; - font-size: 12px; - display: flex; - justify-content: center; - align-items: center; - - &-active { - color: #2b65ff; - } - - & > div { - width: 36px; - padding: 13px 0; - word-break: keep-all; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - cursor: pointer; - } - - &:nth-of-type(1) { - & i { - width: 14px; - height: 14px; - margin-bottom: 4px; - background: url(../../../assets/imgs/ic_tuzhanshi_normal.svg); - - &.selected { - background: url(../../../assets/imgs/ic_tuzhanshi_pressed.svg); - } - } - - & > div:hover { - & i { - background: url(../../../assets/imgs/ic_tuzhanshi_hover.svg); - - &.selected { - background: url(../../../assets/imgs/ic_tuzhanshi_pressed.svg); - } - } - } - } - - &:nth-of-type(2) { - & i { - width: 14px; - height: 14px; - margin-bottom: 4px; - background: url(../../../assets/imgs/ic_biaoge_normal.svg); - - &.selected { - background: url(../../../assets/imgs/ic_biaoge_pressed.svg); - } - } - - & > div:hover { - & i { - background: url(../../../assets/imgs/ic_biaoge_hover.svg); - - &.selected { - background: url(../../../assets/imgs/ic_biaoge_pressed.svg); - } - } - } - } - - &:nth-of-type(3) { - & i { - width: 14px; - height: 14px; - margin-bottom: 4px; - background: url(../../../assets/imgs/ic_json_normal.svg); - - &.selected { - background: url(../../../assets/imgs/ic_json_pressed.svg); - } - } - - & > div:hover { - & i { - background: url(../../../assets/imgs/ic_json_hover.svg); - - &.selected { - background: url(../../../assets/imgs/ic_json_pressed.svg); - } - } - } - } - } - } - - &-content { - width: 100%; - height: 100%; - padding: 8px; - overflow: auto; - position: relative; - - &-table-path-item { - margin-right: 5px; - padding: 3px 8px; - background: #f5f5f5; - border: 1px solid #e0e0e0; - border-radius: 2px; - font-size: 14px; - line-height: 22px; - } - - &-manipulations { - width: 100%; - display: flex; - justify-content: space-between; - padding: 9px; - align-items: center; - font-size: 14px; - position: absolute; - background: #fff; - z-index: 5; - top: 0; - left: 0; - - &.fullscreen { - width: calc(100% - 18px); - } - - & > div:last-child { - display: flex; - align-items: center; - } - - & img { - cursor: pointer; - margin-right: 19.7px; - - &:last-child { - margin-right: 8px; - } - } - } - - &-empty { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - font-size: 14px; - - & > img { - margin-bottom: 10px; - } - - & > span { - display: block; - width: 80%; - text-align: center; - max-height: 200px; - overflow: auto; - } - } - } - - &-filter-options { - width: 100%; - background: #fff; - box-shadow: -4px 0 8px 0 rgba(0, 0, 0, 0.1); - position: absolute; - left: 0; - top: 0; - z-index: 6; - font-size: 14px; - cursor: default; - - &-edge-filter, - &-property-filter { - display: flex; - - & > div { - display: flex; - align-items: center; - margin-right: 24px; - - & > span { - width: 56px; - margin-right: 8px; - } - } - } - - &-edge-filter { - padding: 14px; - - & > div:last-child { - margin-left: auto; - align-items: flex-start; - margin-right: 0; - - & > span { - width: auto; - margin-left: 16px; - margin-right: 0; - color: #2b65ff; - cursor: pointer; - } - } - } - - &-property-filter { - padding: 14px 14px 0; - - & > div { - & > span { - text-align: right; - } - - &:nth-last-child(-n + 2) > span, - &:nth-last-child(-n + 1) > span { - width: auto; - } - - &:nth-last-child(-n + 2) { - margin-right: 16px; - } - } - - &:nth-last-child(-n + 2) { - padding: 14px; - } - } - - &-hr { - margin: 0 auto; - width: calc(100% - 28px); - height: 1px; - background: #e0e0e0; - } - - &-manipulation { - padding: 0 0 14px 78px; - color: #2b65ff; - - & > span { - cursor: pointer; - } - } - } - } - - &-logs-favorite { - .exec-log-status { - width: 63px; - font-size: 12px; - border-radius: 2px; - text-align: center; - - &.success { - background: #f2fff4; - border: 1px solid #7ed988; - color: #39bf45; - } - - &.running { - background: #f2f7ff; - border: 1px solid #8cb8ff; - color: #3d88f2; - } - - &.failed { - background: #fff2f2; - border: 1px solid #ff9499; - color: #e64552; - } - } - - .exec-log-manipulation { - color: #2b65ff; - cursor: pointer; - margin-left: 8px; - - &:first-child { - margin-left: 0; - } - } - - .exec-log-favorite-tab-content-wrapper { - .query-tab-content-wrapper(); - } - - .exec-log-favorite-tab-content { - .query-tab-content(); - } - - &-content { - display: flex; - - &-icon { - height: 16px; - margin-top: 3px; - cursor: pointer; - transition: transform 0.5s; - - &.reverse { - transform: rotate(180deg); - } - } - - &-statements-wrapper { - margin-left: 8px; - display: flex; - flex-direction: column; - } - } - } - - &-graph-node-info { - margin-top: 3px; - font-size: 14px; - line-height: 30px; - color: #333; - - &-item { - display: flex; - - & > div { - word-break: break-all; - } - - & > div:first-child { - flex-basis: 35%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - & > div:last-child { - flex-basis: 65%; - } - - &-disabled { - color: #999; - } - } - } - - &-graph-node-edit { - font-size: 14px; - } - - &-dynamic-add-options { - margin-top: 4px; - font-size: 14px; - line-height: 30px; - color: #333; - } - - &-dynamic-add-option { - display: flex; - align-items: center; - margin-bottom: 32px; - - & > div:first-child { - width: 84px; - line-height: 22px; - margin-right: 24px; - text-align: right; - } - - &-with-expand { - align-items: flex-start; - - & > div:first-child { - line-height: 32px; - } - } - - &-expands { - display: flex; - flex-direction: column; - width: calc(100% - 180px); - } - - &-expand { - display: flex; - margin-bottom: 32px; - - &:first-child { - margin-bottom: 16px; - } - - &:last-child { - margin-bottom: 0; - } - - & > div { - // flex-basis: 50%; - line-height: 32px; - - &:first-child { - width: 162px; - } - } - } - } - - .graph-wrapper { - width: calc(100% + 18px); - position: relative; - height: calc(100vh - 310px); - } - - .full-screen-graph-wrapper { - position: relative; - width: calc(100vw + 18px); - height: calc(100vh + 18px); - } - - .graph-loading-placeholder { - width: 100%; - height: 100%; - background: #fff; - position: absolute; - left: 0; - top: 0; - z-index: 9; - cursor: default; - font-size: 14px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - .graph-pop-over { - width: 126px; - padding: 4px 0; - background: #fff; - border-radius: 3px; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - position: absolute; - left: 0; - top: 0; - z-index: 5; - - &-item { - font-size: 14px; - color: #333; - line-height: 32px; - text-indent: 16px; - cursor: pointer; - - &:hover { - background: #f2f7ff; - } - } - } - - .delete-confirm-wrapper { - width: 320px; - display: flex; - flex-direction: column; - - & > span:first-of-type { - display: block; - line-height: 22px; - margin-bottom: 16px; - font-family: - 'PingFangSC-Medium', - 'Microsoft YaHei', - '微软雅黑', - Arial, - sans-serif; - font-weight: bold; - color: #000; - letter-spacing: 0; - } - - & > span:last-of-type { - font-size: 14px; - color: #333; - margin-bottom: 24px; - } - - & > .delete-confirm-rooter { - width: 100%; - margin-top: 24px; - display: flex; - } - } - - .query-result-loading-bg { - width: 144px; - height: 144px; - position: relative; - margin-bottom: 10px; - } - - .query-result-loading-back { - position: absolute; - left: 22px; - top: 20px; - } - - .query-result-loading-front { - position: absolute; - left: 57.1px; - top: 52.1px; - animation: loading-rotate 2s linear infinite; - } - - @keyframes loading-rotate { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } - } -} - -.tooltip-fields { - display: flex; - max-width: 240px; - - & > div:first-child { - width: 60px; - margin-right: 5px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - & > div:last-child { - max-width: 175px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.metadata-graph-view-tooltip-fields { - .tooltip-fields(); - - max-width: 300px; - - & > div:nth-child(1) { - width: 100px; - } - - & > div:nth-child(2) { - width: 45px; - margin-right: 3px; - } - - & > div:nth-child(3) { - max-width: 60px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -/* override */ -.data-analyze-sidebar-graph-selection.expand .new-fc-one-select:not(.new-fc-one-select-disabled) { - border: 0; -} - -// select from selection of ids(sidebar expanded) -.data-analyze-sidebar-select { - text-indent: 12px; -} - -// dropdown from selection of ids(sidebar collapsed) -.data-analyze-sidebar-dropdown-menu > .new-fc-one-menu-vertical-box.new-fc-one-menu-vertical-medium { - text-align: center; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); -} - -.data-analyze-sidebar-dropdown-menu .new-fc-one-menu-item.new-fc-one-menu-item-disabled { - background-color: #fafafa; - color: #ccc; - cursor: not-allowed; -} - -.new-fc-one-tooltip-inner > .data-analyze-sidebar-menu-item > div { - margin-left: 4px; -} - -.new-fc-one-menu-inline-box .new-fc-one-menu-item-selected { - color: #2b65ff; - border-right: 2px solid #2b65ff; -} - -.query-result-filter-options-property-filter .new-fc-one-btn.new-fc-one-calendar-title.new-fc-one-btn-normal.new-fc-one-btn-medium { - width: 180px; -} - -// use font-size: 14px in large input -.data-analyze .new-fc-one-input-large, -.data-analyze .new-fc-one-input-large + .new-fc-one-input-count { - font-size: 14px; -} - -.data-analyze .new-fc-one-input-error.new-fc-one-input-error-bottom { - font-size: 14px; -} - -.new-fc-one-modal-full-screen { - .new-fc-one-modal-body { - height: 100vh; - max-height: 100vh; - } - - .new-fc-one-modal-content { - height: 100vh; - padding: 0; - } - - .new-fc-one-modal-footer { - margin-top: 0; - } - - .query-result { - margin-top: 0; - height: 100vh; - } -} - -.new-fc-one-table { - font-size: 14px; - line-height: 22px; -} - -.new-fc-one-table-thead > tr > th { - height: 46px; - background: #f5f5f5; - word-break: keep-all; -} - -.new-fc-one-table-tbody > tr > td { - height: 54px; -} - -.CodeMirror { - height: auto; - font-family: - -apple-system, - BlinkMacSystemFont, - 'Helvetica Neue', - Helvetica, - 'PingFang SC', - 'Hiragino Sans GB', - 'Microsoft YaHei', - '微软雅黑', - Arial, - sans-serif; - font-weight: bold; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-size: 14px; - color: #333; - letter-spacing: 0; - line-height: 22px; -} - -.CodeMirror-empty { - color: #999; -} - -.query-tab-content { - .CodeMirror-scroll { - max-height: 120px; - } -} - -.query-tab-code-edit.isLoading { - .CodeMirror { - color: #ccc; - } - - .CodeMirror-lines { - cursor: not-allowed; - } -} - -// override -div.vis-tooltip { - position: absolute; - visibility: hidden; - padding: 12px; - font-size: 12px; - color: #fff; - background-color: rgba(0, 0, 0, 0.8); - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; - pointer-events: none; - z-index: 5; -} - -// refer to index.less, this should be fixed in the future version -.data-analyze-dynamic-add-options .new-fc-one-input-all-container { - position: relative; -} - -.data-analyze-graph-node-info .new-fc-one-input-all-container { - position: relative; -} - -@media screen and (max-width: 1280px) { - .ant-table-tbody > tr > td.sql-content-clazz { - max-width: 480px; - } -} -@media screen and (min-width: 1280px) and (max-width: 1920px) { - .ant-table-tbody > tr > td.sql-content-clazz { - max-width: 600px; - } -} -@media screen and (min-width: 1920px) { - .ant-table-tbody > tr > td.sql-content-clazz { - max-width: 800px; - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyze.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyze.tsx deleted file mode 100644 index a216854c9..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyze.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { useRoute, useLocation, Params } from 'wouter'; -import { Modal, Button } from 'hubble-ui'; - -import DataAnalyzeContent from './DataAnalyzeContent'; -import DataAnalyzeInfoDrawer from './DataAnalyzeInfoDrawer'; -import DynamicAddNode from './DynamicAddNode'; -import DynamicAddEdge from './DynamicAddEdge'; -import { - AppStoreContext, - GraphManagementStoreContext, - DataAnalyzeStoreContext -} from '../../../stores'; -import './DataAnalyze.less'; -import { useTranslation } from 'react-i18next'; - -const DataAnalyze: React.FC = observer(() => { - const { t } = useTranslation(); - const graphManagementStore = useContext(GraphManagementStoreContext); - const appStore = useContext(AppStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const [match, params] = useRoute('/graph-management/:id/data-analyze'); - const [_, setLocation] = useLocation(); - - useEffect(() => { - window.scrollTo(0, 0); - - if (graphManagementStore.requestStatus.fetchIdList !== 'success') { - graphManagementStore.fetchIdList(); - } - - return () => { - dataAnalyzeStore.dispose(); - }; - }, [dataAnalyzeStore, graphManagementStore]); - - // Caution: Preitter will automatically add 'params' behind 'match' in array, - // which is not equal each time - /* eslint-disable */ - useEffect(() => { - if (match && params !== null) { - appStore.setCurrentId(Number(params.id)); - dataAnalyzeStore.setCurrentId(Number(params.id)); - dataAnalyzeStore.fetchValueTypes(); - dataAnalyzeStore.fetchVertexTypes(); - dataAnalyzeStore.fetchAllPropertyIndexes('vertex'); - dataAnalyzeStore.fetchEdgeTypes(); - dataAnalyzeStore.fetchAllNodeStyle(); - dataAnalyzeStore.fetchAllEdgeStyle(); - } - }, [dataAnalyzeStore, match, params?.id]); - - return ( -
    - - - - - { - setLocation('/'); - }} - > - {t('addition.dataAnalyze.return-home')} - - ]} - visible={graphManagementStore.graphData.some( - ({ id, enabled }) => dataAnalyzeStore.currentId === id && !enabled - )} - destroyOnClose - needCloseIcon={false} - > -
    - {dataAnalyzeStore.errorInfo.fetchExecutionLogs.message} -
    -
    -
    - ); -}); - -export default DataAnalyze; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyzeContent.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyzeContent.tsx deleted file mode 100644 index c57f43540..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyzeContent.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useContext } from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; - -import QueryAndAlgorithmLibrary from './QueryAndAlgorithmLibrary'; -import { QueryResult } from './query-result'; -import ExecLogAndQueryCollections from './ExecLogAndQueryCollections'; -import { GraphManagementStoreContext } from '../../../stores'; - -const DataAnalyzeContent: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const [queryResultSidebarIndex, setQueryResultSidebarIndex] = useState(0); - - const wrapperClassName = classnames({ - 'data-analyze-content': true, - 'sidebar-expand': graphManagementStore.isExpanded - }); - - return ( -
    - - - -
    - ); -}); - -export default DataAnalyzeContent; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyzeInfoDrawer.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyzeInfoDrawer.tsx deleted file mode 100644 index 78bb1aa54..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DataAnalyzeInfoDrawer.tsx +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useContext, useCallback, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { isUndefined, update } from 'lodash-es'; -import { Drawer, Input, Button, Message } from 'hubble-ui'; - -import { DataAnalyzeStoreContext } from '../../../stores'; -import { convertArrayToString } from '../../../stores/utils'; -import { useTranslation } from 'react-i18next'; - -const DataAnalyzeInfoDrawer: React.FC = observer(() => { - const { t } = useTranslation(); - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const [isEdit, switchEdit] = useState(false); - - const handleDrawerClose = useCallback(() => { - dataAnalyzeStore.switchShowScreenInfo(false); - switchEdit(false); - }, [dataAnalyzeStore]); - - const premitSave = - !isEdit || - ((dataAnalyzeStore.editedSelectedGraphDataProperties.nonNullable.size === - 0 || - [ - ...dataAnalyzeStore.validateEditableGraphDataPropertyErrorMessage!.nonNullable.values() - ].every((value) => value === '')) && - (dataAnalyzeStore.editedSelectedGraphDataProperties.nullable.size === 0 || - [ - ...dataAnalyzeStore.validateEditableGraphDataPropertyErrorMessage!.nullable.values() - ].every((value) => value === ''))); - - const graphInfoItemClassName = classnames({ - 'data-analyze-graph-node-info-item': true, - 'data-analyze-graph-node-info-item-disabled': isEdit - }); - - useEffect(() => { - const handleOutSideClick = (e: MouseEvent) => { - const drawerWrapper = document.querySelector( - '.new-fc-one-drawer-content-wrapper' - ); - - if ( - !isEdit && - dataAnalyzeStore.isShowGraphInfo && - !dataAnalyzeStore.isClickOnNodeOrEdge && - drawerWrapper && - !drawerWrapper.contains(e.target as Element) - ) { - dataAnalyzeStore.switchShowScreenInfo(false); - } - }; - - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [dataAnalyzeStore, isEdit]); - - return ( - { - if (!isEdit) { - switchEdit(true); - return; - } - - const updatedInfo = await dataAnalyzeStore.updateGraphProperties(); - - if (!isUndefined(updatedInfo)) { - if (dataAnalyzeStore.graphInfoDataSet === 'node') { - dataAnalyzeStore.visDataSet?.nodes.update({ - id: updatedInfo.id, - properties: updatedInfo.properties, - title: ` -
    -
    ${t('addition.common.vertex-type')}:
    -
    ${updatedInfo.label}
    -
    -
    -
    ${t('addition.common.vertex-id')}:
    -
    ${updatedInfo.id}
    -
    - ${Object.entries(updatedInfo.properties) - .map(([key, value]) => { - return `
    -
    ${key}:
    -
    ${convertArrayToString(value)}
    -
    `; - }) - .join('')} - ` - }); - } - - if (dataAnalyzeStore.graphInfoDataSet === 'edge') { - dataAnalyzeStore.visDataSet?.edges.update({ - id: updatedInfo.id, - properties: updatedInfo.properties, - title: ` -
    -
    ${t('addition.common.edge-type')}:
    -
    ${updatedInfo.label}
    -
    -
    -
    ${t('addition.common.edge-id')}:
    -
    ${updatedInfo.id}
    -
    - ${Object.entries(updatedInfo.properties) - .map(([key, value]) => { - return `
    -
    ${key}:
    -
    ${convertArrayToString(value)}
    -
    - `; - }) - .join('')} - ` - }); - } - } - - if ( - dataAnalyzeStore.requestStatus.updateGraphProperties === 'success' - ) { - Message.success({ - content: t('addition.common.save-scuccess'), - size: 'medium', - showCloseIcon: false - }); - } else { - Message.error({ - content: t('addition.common.save-fail'), - size: 'medium', - showCloseIcon: false - }); - } - - handleDrawerClose(); - }} - key="save" - > - {isEdit ? t('addition.common.save') : t('addition.common.edit')} - , - - ]} - > -
    - {dataAnalyzeStore.graphInfoDataSet === 'node' ? ( - <> -
    -
    {t('addition.common.vertex-type')}:
    -
    {dataAnalyzeStore.selectedGraphData.label}
    -
    -
    -
    {t('addition.common.vertex-id')}:
    -
    {dataAnalyzeStore.selectedGraphData.id}
    -
    - - ) : ( - <> -
    -
    {t('addition.common.edge-type')}:
    -
    {dataAnalyzeStore.selectedGraphLinkData.label}
    -
    -
    -
    {t('addition.common.edge-id')}:
    -
    {dataAnalyzeStore.selectedGraphLinkData.id}
    -
    -
    -
    {t('addition.common.source')}:
    -
    {dataAnalyzeStore.selectedGraphLinkData.source}
    -
    -
    -
    {t('addition.common.target')}:
    -
    {dataAnalyzeStore.selectedGraphLinkData.target}
    -
    - - )} - - {isEdit && - (dataAnalyzeStore.editedSelectedGraphDataProperties.primary.size !== - 0 || - dataAnalyzeStore.editedSelectedGraphDataProperties.nonNullable - .size !== 0) && ( -
    - {t('addition.common.required-property')}: -
    - )} - {[...dataAnalyzeStore.editedSelectedGraphDataProperties.primary].map( - ([key, value], primaryKeyIndex) => ( -
    -
    - {key}( - {dataAnalyzeStore.graphInfoDataSet === 'node' - ? t('addition.common.primary-key') - : t('addition.common.distinguishing-key')} - {`${primaryKeyIndex + 1}`}): -
    -
    {value}
    -
    - ) - )} - {[ - ...dataAnalyzeStore.editedSelectedGraphDataProperties.nonNullable - ].map(([key, value], nonNullableIndex) => ( -
    -
    {key}:
    - {!isEdit ? ( -
    {convertArrayToString(value)}
    - ) : ( -
    - { - dataAnalyzeStore.editGraphDataProperties( - 'nonNullable', - key, - e.value - ); - - dataAnalyzeStore.validateGraphDataEditableProperties( - 'nonNullable', - key - ); - }} - onBlur={() => { - dataAnalyzeStore.validateGraphDataEditableProperties( - 'nonNullable', - key - ); - }} - /> -
    - )} -
    - ))} - - {isEdit && - dataAnalyzeStore.editedSelectedGraphDataProperties.nullable.size !== - 0 && ( -
    - {t('addition.common.nullable-property')}: -
    - )} - {[...dataAnalyzeStore.editedSelectedGraphDataProperties.nullable].map( - ([key, value], nullableIndex) => ( -
    -
    {key}:
    - {!isEdit ? ( -
    {convertArrayToString(value)}
    - ) : ( -
    - { - dataAnalyzeStore.editGraphDataProperties( - 'nullable', - key, - e.value - ); - - dataAnalyzeStore.validateGraphDataEditableProperties( - 'nullable', - key - ); - }} - onBlur={() => { - dataAnalyzeStore.validateGraphDataEditableProperties( - 'nullable', - key - ); - }} - /> -
    - )} -
    - ) - )} -
    -
    - ); -}); - -export default DataAnalyzeInfoDrawer; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DynamicAddEdge.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DynamicAddEdge.tsx deleted file mode 100644 index eb9299e1c..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DynamicAddEdge.tsx +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback, useRef, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { Drawer, Select, Input, Button, Message } from 'hubble-ui'; - -import { DataAnalyzeStoreContext } from '../../../stores'; -import { addGraphNodes, addGraphEdges } from '../../../stores/utils'; -import { isUndefined, isEmpty } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import i18next from '../../../i18n'; - -const IDStrategyMappings: Record = { - PRIMARY_KEY: i18next.t('addition.constant.primary-key-id'), - AUTOMATIC: i18next.t('addition.constant.automatic-generation'), - CUSTOMIZE_STRING: i18next.t('addition.constant.custom-string'), - CUSTOMIZE_NUMBER: i18next.t('addition.constant.custom-number'), - CUSTOMIZE_UUID: i18next.t('addition.constant.custom-uuid') -}; - -const DataAnalyzeAddEdge: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { t } = useTranslation(); - - const title = - dataAnalyzeStore.dynamicAddGraphDataStatus === 'inEdge' - ? t('addition.common.in-edge') - : t('addition.common.out-edge'); - - const selectedEdgeLabel = dataAnalyzeStore.edgeTypes.find( - ({ name }) => name === dataAnalyzeStore.newGraphEdgeConfigs.label - )!; - - const nonNullableProperties = [ - ...dataAnalyzeStore.newGraphEdgeConfigs.properties.nonNullable.keys() - ]; - - const nullableProperties = [ - ...dataAnalyzeStore.newGraphEdgeConfigs.properties.nullable.keys() - ]; - - const premitAddGraphEdge = - !isEmpty(dataAnalyzeStore.newGraphEdgeConfigs.label) && - !isEmpty(dataAnalyzeStore.newGraphEdgeConfigs.id) && - ![ - ...dataAnalyzeStore.newGraphEdgeConfigs.properties.nonNullable.values() - ].includes('') && - [ - ...dataAnalyzeStore.validateAddGraphEdgeErrorMessage!.properties.nonNullable.values() - ].every((value) => value === '') && - [ - ...dataAnalyzeStore.validateAddGraphEdgeErrorMessage!.properties.nullable.values() - ].every((value) => value === ''); - - const handleDrawerClose = useCallback(() => { - dataAnalyzeStore.setDynamicAddGraphDataStatus(''); - dataAnalyzeStore.resetNewGraphData('edge'); - }, [dataAnalyzeStore]); - - useEffect(() => { - const handleOutSideClick = (e: MouseEvent) => { - const drawerWrapper = document.querySelector( - '.new-fc-one-drawer-content-wrapper' - ); - - if ( - dataAnalyzeStore.isShowGraphInfo && - !dataAnalyzeStore.isClickOnNodeOrEdge && - drawerWrapper && - !drawerWrapper.contains(e.target as Element) - ) { - dataAnalyzeStore.setDynamicAddGraphDataStatus(''); - } - }; - - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [dataAnalyzeStore]); - - return ( - { - const graphViewData = await dataAnalyzeStore.addGraphEdge(); - - if (!isUndefined(graphViewData)) { - const { vertices, originalVertices, edges } = graphViewData; - - addGraphNodes( - vertices, - dataAnalyzeStore.visDataSet?.nodes, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexWritingMappings - ); - - addGraphEdges( - edges, - dataAnalyzeStore.visDataSet?.edges, - dataAnalyzeStore.edgeColorMappings, - dataAnalyzeStore.edgeThicknessMappings, - dataAnalyzeStore.edgeWithArrowMappings, - dataAnalyzeStore.edgeWritingMappings - ); - - dataAnalyzeStore.visNetwork?.selectNodes( - originalVertices.map(({ id }) => id) - ); - - dataAnalyzeStore.visNetwork?.selectEdges( - edges.map(({ id }) => id) - ); - - Message.success({ - content: t('addition.common.add-success'), - size: 'medium', - showCloseIcon: false - }); - } else { - Message.error({ - content: t('addition.common.add-fail'), - size: 'medium', - showCloseIcon: false - }); - } - - handleDrawerClose(); - }} - > - {t('addition.common.add')} - , - - ]} - > -
    -
    -
    - {title === t('addition.common.out-edge') - ? t('addition.common.source') - : t('addition.common.target')} - : -
    - {dataAnalyzeStore.rightClickedGraphData.id} -
    -
    -
    {t('addition.common.edge-type')}:
    - -
    - - {!isUndefined(selectedEdgeLabel) && ( - <> -
    -
    - {title === t('addition.common.out-edge') - ? t('addition.common.target') - : t('addition.common.source')} - : -
    -
    - { - dataAnalyzeStore.setNewGraphDataConfig( - 'edge', - 'id', - e.value - ); - - dataAnalyzeStore.validateAddGraphEdge('id', false); - }} - onBlur={() => { - dataAnalyzeStore.validateAddGraphEdge('id', false); - }} - /> -
    -
    - - {dataAnalyzeStore.newGraphEdgeConfigs.properties.nonNullable - .size !== 0 && ( -
    -
    {t('addition.common.required-property')}:
    -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.property-value')}
    -
    - {nonNullableProperties.map((property) => ( -
    -
    {property}
    -
    - { - dataAnalyzeStore.setNewGraphDataConfigProperties( - 'edge', - 'nonNullable', - property, - e.value - ); - - dataAnalyzeStore.validateAddGraphEdge( - 'nonNullable', - false, - property - ); - }} - onBlur={() => { - dataAnalyzeStore.validateAddGraphEdge( - 'nonNullable', - false, - property - ); - }} - /> -
    -
    - ))} -
    -
    - )} - - {dataAnalyzeStore.newGraphEdgeConfigs.properties.nullable.size !== - 0 && ( -
    -
    {t('addition.common.nullable-property')}:
    -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.property-value')}
    -
    - {nullableProperties.map((property) => ( -
    -
    {property}
    -
    - { - dataAnalyzeStore.setNewGraphDataConfigProperties( - 'edge', - 'nullable', - property, - e.value - ); - - dataAnalyzeStore.validateAddGraphEdge( - 'nullable', - false, - property - ); - }} - onBlur={() => { - dataAnalyzeStore.validateAddGraphEdge( - 'nullable', - false, - property - ); - }} - /> -
    -
    - ))} -
    -
    - )} - - )} -
    -
    - ); -}); - -export default DataAnalyzeAddEdge; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DynamicAddNode.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DynamicAddNode.tsx deleted file mode 100644 index 10ad81974..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/DynamicAddNode.tsx +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback, useRef, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { Drawer, Select, Input, Button, Message } from 'hubble-ui'; - -import { DataAnalyzeStoreContext } from '../../../stores'; -import { addGraphNodes } from '../../../stores/utils'; -import { isUndefined, isEmpty } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import i18next from '../../../i18n'; - -const IDStrategyMappings: Record = { - PRIMARY_KEY: i18next.t('addition.constant.primary-key-id'), - AUTOMATIC: i18next.t('addition.constant.automatic-generation'), - CUSTOMIZE_STRING: i18next.t('addition.constant.custom-string'), - CUSTOMIZE_NUMBER: i18next.t('addition.constant.custom-number'), - CUSTOMIZE_UUID: i18next.t('addition.constant.custom-uuid') -}; - -const DataAnalyzeAddNode: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { t } = useTranslation(); - - const selectedVertexLabel = dataAnalyzeStore.vertexTypes.find( - ({ name }) => name === dataAnalyzeStore.newGraphNodeConfigs.label - )!; - - const nonNullableProperties = [ - ...dataAnalyzeStore.newGraphNodeConfigs.properties.nonNullable.keys() - ]; - - const nullableProperties = [ - ...dataAnalyzeStore.newGraphNodeConfigs.properties.nullable.keys() - ]; - - const shouldRevealId = - selectedVertexLabel && - selectedVertexLabel!.id_strategy !== 'PRIMARY_KEY' && - selectedVertexLabel!.id_strategy !== 'AUTOMATIC'; - - const premitAddGraphNode = - !isEmpty(dataAnalyzeStore.newGraphNodeConfigs.label) && - ![ - ...dataAnalyzeStore.newGraphNodeConfigs.properties.nonNullable.values() - ].includes('') && - [ - ...dataAnalyzeStore.validateAddGraphNodeErrorMessage!.properties.nonNullable.values() - ].every((value) => value === '') && - [ - ...dataAnalyzeStore.validateAddGraphNodeErrorMessage!.properties.nullable.values() - ].every((value) => value === '') && - (shouldRevealId - ? !isEmpty(dataAnalyzeStore.newGraphNodeConfigs.id) && - isEmpty(dataAnalyzeStore.validateAddGraphNodeErrorMessage!.id) - : true); - - const handleDrawerClose = useCallback(() => { - dataAnalyzeStore.setDynamicAddGraphDataStatus(''); - dataAnalyzeStore.resetNewGraphData('vertex'); - }, [dataAnalyzeStore]); - - useEffect(() => { - const handleOutSideClick = (e: MouseEvent) => { - const drawerWrapper = document.querySelector( - '.new-fc-one-drawer-content-wrapper' - ); - - if ( - dataAnalyzeStore.isShowGraphInfo && - !dataAnalyzeStore.isClickOnNodeOrEdge && - drawerWrapper && - !drawerWrapper.contains(e.target as Element) - ) { - dataAnalyzeStore.setDynamicAddGraphDataStatus(''); - } - }; - - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [dataAnalyzeStore]); - - return ( - { - const newGraphNodes = await dataAnalyzeStore.addGraphNode(); - - if (!isUndefined(newGraphNodes)) { - const node = dataAnalyzeStore.visDataSet?.nodes.get( - newGraphNodes[0].id - ); - - if (node !== null) { - dataAnalyzeStore.visNetwork?.selectNodes([node.id]); - dataAnalyzeStore.visNetwork?.focus(node.id, { - animation: true - }); - - handleDrawerClose(); - return; - } - - addGraphNodes( - newGraphNodes, - dataAnalyzeStore.visDataSet?.nodes, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexWritingMappings - ); - - dataAnalyzeStore.visNetwork?.selectNodes( - newGraphNodes.map(({ id }) => id) - ); - - Message.success({ - content: t('addition.common.add-success'), - size: 'medium', - showCloseIcon: false - }); - } else { - Message.error({ - content: t('addition.common.add-fail'), - size: 'medium', - showCloseIcon: false - }); - } - - handleDrawerClose(); - }} - > - {t('addition.common.add')} - , - - ]} - > -
    -
    -
    {t('addition.common.id-strategy')}:
    - -
    - - {!isUndefined(selectedVertexLabel) && ( - <> -
    -
    {t('addition.common.id-strategy')}
    - - {selectedVertexLabel.id_strategy === 'PRIMARY_KEY' - ? `${t( - 'addition.common.primary-key-property' - )}-${selectedVertexLabel.primary_keys.join(',')}` - : IDStrategyMappings[selectedVertexLabel.id_strategy]} - -
    - {shouldRevealId && ( -
    -
    - {t('addition.common.id-value')}: -
    -
    - { - dataAnalyzeStore.setNewGraphDataConfig( - 'vertex', - 'id', - e.value - ); - - dataAnalyzeStore.validateAddGraphNode( - selectedVertexLabel.id_strategy, - false, - 'id' - ); - }} - onBlur={() => { - dataAnalyzeStore.validateAddGraphNode( - selectedVertexLabel.id_strategy, - false, - 'id' - ); - }} - /> -
    -
    - )} - - {dataAnalyzeStore.newGraphNodeConfigs.properties.nonNullable - .size !== 0 && ( -
    -
    {t('addition.common.required-property')}:
    -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.property-value')}
    -
    - {nonNullableProperties.map((property) => ( -
    -
    {property}
    -
    - { - dataAnalyzeStore.setNewGraphDataConfigProperties( - 'vertex', - 'nonNullable', - property, - e.value - ); - - dataAnalyzeStore.validateAddGraphNode( - selectedVertexLabel.id_strategy, - false, - 'nonNullable', - property - ); - }} - onBlur={() => { - dataAnalyzeStore.validateAddGraphNode( - selectedVertexLabel.id_strategy, - false, - 'nonNullable', - property - ); - }} - /> -
    -
    - ))} -
    -
    - )} - - {dataAnalyzeStore.newGraphNodeConfigs.properties.nullable.size !== - 0 && ( -
    -
    {t('addition.common.nullable-property')}:
    -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.property-value')}
    -
    - {nullableProperties.map((property) => ( -
    -
    {property}
    -
    - { - dataAnalyzeStore.setNewGraphDataConfigProperties( - 'vertex', - 'nullable', - property, - e.value - ); - - dataAnalyzeStore.validateAddGraphNode( - selectedVertexLabel.id_strategy, - false, - 'nullable', - property - ); - }} - onBlur={() => { - dataAnalyzeStore.validateAddGraphNode( - selectedVertexLabel.id_strategy, - false, - 'nullable', - property - ); - }} - /> -
    -
    - ))} -
    -
    - )} - - )} -
    -
    - ); -}); - -export default DataAnalyzeAddNode; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/ExecLogAndQueryCollections.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/ExecLogAndQueryCollections.tsx deleted file mode 100644 index e42c963e1..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/ExecLogAndQueryCollections.tsx +++ /dev/null @@ -1,1088 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useContext, useEffect, useCallback } from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { isUndefined, cloneDeep, isEmpty, isNull } from 'lodash-es'; -import Highlighter from 'react-highlight-words'; -import { useRoute, useLocation } from 'wouter'; -import { useTranslation } from 'react-i18next'; - -import { v4 } from 'uuid'; -import { Table, Input, Button, Message } from 'hubble-ui'; - -import { Tooltip } from '../../common'; -import Favorite from './common/Favorite'; -import { - DataAnalyzeStoreContext, - AsyncTasksStoreContext -} from '../../../stores'; -import { formatAlgorithmStatement } from '../../../utils'; - -import type { - ExecutionLogs, - FavoriteQuery, - LoopDetectionParams, - FocusDetectionParams, - ShortestPathAlgorithmParams, - ShortestPathAllAlgorithmParams, - AllPathAlgorithmParams, - KStepNeighbor, - KHop, - RadiographicInspection, - SameNeighbor, - Jaccard, - WeightedShortestPath, - SingleSourceWeightedShortestPath, - PersonalRank, - ModelSimilarityParams, - NeighborRankParams, - CustomPathParams, - CustomPathRule -} from '../../../stores/types/GraphManagementStore/dataAnalyzeStore'; -import { Algorithm } from '../../../stores/factory/dataAnalyzeStore/algorithmStore'; - -import ArrowIcon from '../../../assets/imgs/ic_arrow_16.svg'; -import EmptyIcon from '../../../assets/imgs/ic_sousuo_empty.svg'; -import { toJS } from 'mobx'; - -export const AlgorithmInternalNameMapping: Record = { - rings: 'loop-detection', - crosspoints: 'focus-detection', - shortpath: 'shortest-path', - allshortpath: 'shortest-path-all', - paths: 'all-path', - fsimilarity: 'model-similarity', - neighborrank: 'neighbor-rank', - kneighbor: 'k-step-neighbor', - kout: 'k-hop', - customizedpaths: 'custom-path', - rays: 'radiographic-inspection', - sameneighbors: 'same-neighbor', - weightedshortpath: 'weighted-shortest-path', - singleshortpath: 'single-source-weighted-shortest-path', - jaccardsimilarity: 'jaccard', - personalrank: 'personal-rank' -}; - -const styles = { - tableCenter: { - display: 'flex', - justifyContent: 'center' - }, - favoriteQueriesWrapper: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-end' - } -}; - -const ExecLogAndQueryCollections: React.FC = observer(() => { - const asyncTasksStore = useContext(AsyncTasksStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { algorithmAnalyzerStore } = dataAnalyzeStore; - const [tabIndex, setTabIndex] = useState(0); - const [, params] = useRoute('/graph-management/:id/data-analyze'); - const { t } = useTranslation(); - const [, setLocation] = useLocation(); - - // popovers - const [isFavoritePop, switchFavoritePop] = useState(false); - const [currentPopInTable, setCurrentPopInTable] = useState< - 'execLogs' | 'favoriteQueries' | 'deleteQueries' - >('execLogs'); - - const [currentFavoritePop, setCurrentFavoritePop] = useState( - null - ); - - // hack: @observable in columnConfigs cannot be observed - dataAnalyzeStore.favoritePopUp.toUpperCase(); - - const execLogsColumnConfigs = [ - { - title: t('data-analyze.exec-logs.table-title.time'), - dataIndex: 'create_time', - width: '20%' - }, - { - title: t('data-analyze.exec-logs.table-title.type'), - dataIndex: 'type', - width: '15%', - render(type: string) { - return t(`data-analyze.exec-logs.type.${type}`); - } - }, - { - title: t('data-analyze.exec-logs.table-title.content'), - dataIndex: 'content', - className: 'sql-content-clazz', - width: '30%', - render(text: string, rowData: ExecutionLogs) { - return ( - - ); - } - }, - { - title: t('data-analyze.exec-logs.table-title.status'), - dataIndex: 'status', - width: '10%', - align: 'center', - render(text: string) { - switch (text) { - case 'SUCCESS': - return ( -
    -
    - {t('data-analyze.exec-logs.status.success')} -
    -
    - ); - case 'ASYNC_TASK_SUCCESS': - return ( -
    -
    - {t('data-analyze.exec-logs.status.async-success')} -
    -
    - ); - case 'RUNNING': - return ( -
    -
    - {t('data-analyze.exec-logs.status.running')} -
    -
    - ); - case 'ASYNC_TASK_RUNNING': - return ( -
    -
    - {t('data-analyze.exec-logs.status.running')} -
    -
    - ); - case 'FAILED': - return ( -
    -
    - {t('data-analyze.exec-logs.status.failed')} -
    -
    - ); - case 'ASYNC_TASK_FAILED': - return ( -
    -
    - {t('data-analyze.exec-logs.status.async-failed')} -
    -
    - ); - } - } - }, - { - title: t('data-analyze.exec-logs.table-title.duration'), - dataIndex: 'duration', - align: 'right', - width: '10%' - }, - { - title: t('data-analyze.exec-logs.table-title.manipulation'), - dataIndex: 'manipulation', - width: '15%', - render(_: string, rowData: ExecutionLogs, index: number) { - return ( -
    - {rowData.type === 'GREMLIN_ASYNC' && ( - { - // mock search - asyncTasksStore.mutateSearchWords(String(rowData.async_id)); - asyncTasksStore.switchSearchedStatus(true); - - setLocation(`/graph-management/${params!.id}/async-tasks`); - }} - > - {t('addition.operate.detail')} - - )} - - } - childrenProps={{ - className: 'exec-log-manipulation', - onClick: () => { - dataAnalyzeStore.setFavoritePopUp('addFavoriteInExeclog'); - dataAnalyzeStore.resetFavoriteRequestStatus('add'); - dataAnalyzeStore.resetFavoriteRequestStatus('edit'); - switchFavoritePop(true); - setCurrentFavoritePop(index); - } - }} - > - {t('addition.operate.favorite')} - - - {t('addition.operate.load-statement')} - -
    - ); - } - } - ]; - - const queryFavoriteColumnConfigs = [ - { - title: t('addition.operate.time'), - dataIndex: 'create_time', - width: '25%', - sorter: true - }, - { - title: t('addition.operate.name'), - dataIndex: 'name', - width: '15%', - sorter: true, - render(text: string) { - return ( - - ); - } - }, - { - title: t('addition.operate.favorite-statement'), - dataIndex: 'content', - width: '40%', - className: 'sql-content-clazz', - render(text: string, rowData: FavoriteQuery) { - return ( - - ); - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: string, rowData: FavoriteQuery, index: number) { - return ( -
    - - {t('addition.operate.load-statement')} - - - } - childrenProps={{ - className: 'exec-log-manipulation', - onClick: () => { - dataAnalyzeStore.setFavoritePopUp('editFavorite'); - dataAnalyzeStore.resetFavoriteRequestStatus('add'); - dataAnalyzeStore.resetFavoriteRequestStatus('edit'); - setCurrentPopInTable('favoriteQueries'); - switchFavoritePop(true); - setCurrentFavoritePop(index); - } - }} - > - {t('addition.operate.modify-name')} - - - } - childrenProps={{ - className: 'exec-log-manipulation', - onClick: () => { - setCurrentPopInTable('deleteQueries'); - switchFavoritePop(true); - setCurrentFavoritePop(index); - } - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - const handleExecLogPageNoChange = useCallback( - (e: React.ChangeEvent) => { - dataAnalyzeStore.mutatePageNumber('executionLog', Number(e.target.value)); - dataAnalyzeStore.fetchExecutionLogs(); - }, - [dataAnalyzeStore] - ); - - const handleFavoriteQueriesPageNoChange = useCallback( - (e: React.ChangeEvent) => { - dataAnalyzeStore.mutatePageNumber( - 'favoriteQueries', - Number(e.target.value) - ); - dataAnalyzeStore.fetchFavoriteQueries(); - }, - [dataAnalyzeStore] - ); - - const handleExecLogPageSizeChange = useCallback( - (e: React.ChangeEvent) => { - dataAnalyzeStore.mutatePageSize('executionLog', Number(e.target.value)); - dataAnalyzeStore.mutatePageNumber('executionLog', 1); - dataAnalyzeStore.fetchExecutionLogs(); - }, - [dataAnalyzeStore] - ); - - const handleFavoriteQueriesPageSizeChange = useCallback( - (e: React.ChangeEvent) => { - dataAnalyzeStore.mutatePageSize( - 'favoriteQueries', - Number(e.target.value) - ); - dataAnalyzeStore.mutatePageNumber('favoriteQueries', 1); - dataAnalyzeStore.fetchFavoriteQueries(); - }, - [dataAnalyzeStore] - ); - - const loadStatements = useCallback( - ( - content: string, - type?: 'query' | 'task' | 'algorithm', - algorithmName?: string - ) => () => { - if (!dataAnalyzeStore.isLoadingGraph) { - if (type === 'algorithm') { - const params: Record = JSON.parse(content); - dataAnalyzeStore.setCurrentTab('algorithm-analyze'); - - if (params.hasOwnProperty('direction')) { - params.direction = params.direction.toUpperCase(); - } - - if (params.hasOwnProperty('label')) { - params['label'] === null && (params['label'] = '__all__'); - } - - window.scrollTo(0, 0); - - switch (algorithmName) { - case 'rings': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.loopDetection - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateLoopDetectionParams( - key as keyof LoopDetectionParams, - params[key] - ); - }); - - return; - case 'crosspoints': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.focusDetection - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateFocusDetectionParams( - key as keyof FocusDetectionParams, - params[key] - ); - }); - - return; - case 'shortpath': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.shortestPath - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateShortestPathParams( - key as keyof ShortestPathAlgorithmParams, - params[key] - ); - }); - - return; - case 'allshortpath': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.shortestPathAll - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateShortestPathAllParams( - key as keyof ShortestPathAllAlgorithmParams, - params[key] - ); - }); - - return; - case 'paths': - algorithmAnalyzerStore.changeCurrentAlgorithm(Algorithm.allPath); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateAllPathParams( - key as keyof AllPathAlgorithmParams, - params[key] - ); - }); - - return; - case 'fsimilarity': { - const clonedParams = cloneDeep(params); - - if (isEmpty(params.sources.ids)) { - clonedParams.method = 'property'; - } else { - clonedParams.method = 'id'; - } - - clonedParams.source = clonedParams.sources.ids.join(','); - clonedParams.vertexType = clonedParams.sources.label; - clonedParams.vertexProperty = Object.entries( - clonedParams.sources.properties - ); - - if (isEmpty(clonedParams.vertexProperty)) { - clonedParams.vertexProperty = [['', '']]; - } else { - clonedParams.vertexProperty = clonedParams.vertexProperty.map( - ([key, value]: [string, string[]]) => [key, value.join(',')] - ); - } - - clonedParams.least_neighbor = clonedParams.min_neighbors; - clonedParams.similarity = clonedParams.alpha; - clonedParams.least_similar = clonedParams.min_similars; - clonedParams.max_similar = clonedParams.top; - clonedParams.property_filter = clonedParams.group_property; - clonedParams.least_property_number = clonedParams.min_groups; - clonedParams.return_common_connection = - clonedParams.with_intermediary; - clonedParams.return_complete_info = clonedParams.with_vertex; - - delete clonedParams.sources; - delete clonedParams.min_neighbors; - delete clonedParams.alpha; - delete clonedParams.min_similars; - delete clonedParams.top; - delete clonedParams.group_property; - delete clonedParams.min_groups; - delete clonedParams.with_intermediary; - delete clonedParams.with_vertex; - - Object.keys(clonedParams).forEach((key) => { - algorithmAnalyzerStore.mutateModelSimilarityParams( - key as keyof ModelSimilarityParams, - clonedParams[key] - ); - }); - - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.modelSimilarity - ); - - return; - } - case 'neighborrank': { - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.neighborRank - ); - - const clonedParams = cloneDeep(params) as NeighborRankParams; - - // remove neighborrank validate rules first; - algorithmAnalyzerStore.removeValidateNeighborRankRule(0); - clonedParams.steps.forEach(({ labels }, index) => { - clonedParams.steps[index].uuid = v4(); - // add neighborrank rule validation per each - algorithmAnalyzerStore.addValidateNeighborRankRule(); - - if (isEmpty(labels)) { - clonedParams.steps[index].labels = ['__all__']; - } - }); - - Object.keys(clonedParams).forEach((key) => { - algorithmAnalyzerStore.mutateNeighborRankParams( - key as keyof NeighborRankParams, - // @ts-ignore - clonedParams[key] - ); - }); - - return; - } - case 'kneighbor': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.kStepNeighbor - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateKStepNeighborParams( - key as keyof KStepNeighbor, - params[key] - ); - }); - - return; - case 'kout': - algorithmAnalyzerStore.changeCurrentAlgorithm(Algorithm.kHop); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateKHopParams( - key as keyof KHop, - params[key] - ); - }); - - return; - case 'customizedpaths': - const clonedParams = cloneDeep(params); - - if (isEmpty(params.sources.ids)) { - clonedParams.method = 'property'; - } else { - clonedParams.method = 'id'; - } - clonedParams.source = clonedParams.sources.ids.join(','); - clonedParams.vertexType = clonedParams.sources.label; - clonedParams.vertexProperty = Object.entries( - clonedParams.sources.properties - ); - - if (isEmpty(clonedParams.vertexProperty)) { - clonedParams.vertexProperty = [['', '']]; - } else { - clonedParams.vertexProperty = clonedParams.vertexProperty.map( - ([key, value]: [string, string[]]) => [key, value.join(',')] - ); - - clonedParams.vertexProperty = Object.keys( - clonedParams.vertexProperty - ).map((key) => [ - key, - clonedParams.vertexProperty[key].join(',') - ]); - } - - clonedParams.steps.forEach((step: any, index: number) => { - const { labels, properties, weight_by, default_weight } = step; - clonedParams.steps[index].uuid = v4(); - - if (isEmpty(labels)) { - step.labels = dataAnalyzeStore.edgeTypes.map( - ({ name }) => name - ); - } - - if (isEmpty(properties)) { - step.properties = [['', '']]; - } else { - step.properties = Object.keys(step.properties).map((key) => [ - key, - step.properties[key].join(',') - ]); - } - - if (clonedParams.sort_by === 'NONE') { - step.weight_by = ''; - step.default_weight = ''; - } else { - if (!isNull(weight_by)) { - step.default_weight = ''; - } else { - // custom weight - step.weight_by = '__CUSTOM_WEIGHT__'; - step.default_weight = default_weight; - } - } - }); - - delete clonedParams.sources; - - Object.keys(clonedParams).forEach((key) => { - algorithmAnalyzerStore.mutateCustomPathParams( - key as keyof CustomPathParams, - clonedParams[key] - ); - }); - - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.customPath - ); - - return; - case 'rays': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.radiographicInspection - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateRadiographicInspectionParams( - key as keyof RadiographicInspection, - params[key] - ); - }); - - return; - case 'sameneighbors': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.sameNeighbor - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateSameNeighborParams( - key as keyof SameNeighbor, - params[key] - ); - }); - - return; - case 'weightedshortpath': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.weightedShortestPath - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - key as keyof WeightedShortestPath, - params[key] - ); - }); - - return; - case 'singleshortpath': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.singleSourceWeightedShortestPath - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - key as keyof SingleSourceWeightedShortestPath, - params[key] - ); - }); - - return; - case 'jaccardsimilarity': - algorithmAnalyzerStore.changeCurrentAlgorithm(Algorithm.jaccard); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutateJaccardParams( - key as keyof Jaccard, - params[key] - ); - }); - - return; - case 'personalrank': - algorithmAnalyzerStore.changeCurrentAlgorithm( - Algorithm.personalRankRecommendation - ); - - Object.keys(params).forEach((key) => { - algorithmAnalyzerStore.mutatePersonalRankParams( - key as keyof PersonalRank, - params[key] - ); - }); - - return; - default: - return; - } - } - - if (!isUndefined(type)) { - type === 'task' - ? dataAnalyzeStore.setQueryMode('task') - : dataAnalyzeStore.setQueryMode('query'); - } - - switchFavoritePop(false); - dataAnalyzeStore.mutateCodeEditorText(content); - dataAnalyzeStore.triggerLoadingStatementsIntoEditor(); - window.scrollTo(0, 0); - } - }, - [dataAnalyzeStore] - ); - - const handleSearchChange = useCallback( - (e: React.ChangeEvent) => { - dataAnalyzeStore.mutateSearchText(e.target.value); - }, - [dataAnalyzeStore] - ); - - const handleSearch = useCallback(() => { - dataAnalyzeStore.mutatePageNumber('favoriteQueries', 1); - dataAnalyzeStore.swtichIsSearchedStatus(true); - dataAnalyzeStore.fetchFavoriteQueries(); - }, [dataAnalyzeStore]); - - const handleClearSearch = useCallback(() => { - dataAnalyzeStore.mutateSearchText(''); - dataAnalyzeStore.mutatePageNumber('favoriteQueries', 1); - dataAnalyzeStore.swtichIsSearchedStatus(false); - dataAnalyzeStore.fetchFavoriteQueries(); - }, [dataAnalyzeStore]); - - const handleFavoriteSortClick = useCallback( - ({ sortOrder, sortColumn }) => { - if (sortColumn.dataIndex === 'create_time') { - dataAnalyzeStore.changeFavoriteQueriesSortOrder('name', ''); - - sortOrder === 'descend' - ? dataAnalyzeStore.changeFavoriteQueriesSortOrder('time', 'desc') - : dataAnalyzeStore.changeFavoriteQueriesSortOrder('time', 'asc'); - } - - if (sortColumn.dataIndex === 'name') { - dataAnalyzeStore.changeFavoriteQueriesSortOrder('time', ''); - - sortOrder === 'descend' - ? dataAnalyzeStore.changeFavoriteQueriesSortOrder('name', 'desc') - : dataAnalyzeStore.changeFavoriteQueriesSortOrder('name', 'asc'); - } - - dataAnalyzeStore.fetchFavoriteQueries(); - }, - [dataAnalyzeStore] - ); - - useEffect(() => { - if (dataAnalyzeStore.currentId !== null) { - dataAnalyzeStore.fetchExecutionLogs(); - dataAnalyzeStore.fetchFavoriteQueries(); - } - // fetch execlogs & favorites after id changes - }, [dataAnalyzeStore, dataAnalyzeStore.currentId]); - - return ( -
    -
    -
    { - setTabIndex(0); - setCurrentPopInTable('execLogs'); - }} - className={ - tabIndex === 0 ? 'query-tab-index active' : 'query-tab-index' - } - > - {t('addition.operate.execution-record')} -
    -
    { - setTabIndex(1); - setCurrentPopInTable('favoriteQueries'); - }} - className={ - tabIndex === 1 ? 'query-tab-index active' : 'query-tab-index' - } - > - {t('addition.operate.favorite-queries')} -
    -
    -
    -
    -
    - {tabIndex === 0 ? ( -
    - ) : ( -
    -
    - -
    -
    - {t('addition.common.no-matching-results')} -
    - {t('addition.common.no-matching-results')} -
    - - ) : ( - t('addition.common.no-data') - ) - }} - columns={queryFavoriteColumnConfigs} - dataSource={dataAnalyzeStore.favoriteQueryData} - onSortClick={handleFavoriteSortClick} - pagination={{ - showPageJumper: false, - pageSize: - dataAnalyzeStore.pageConfigs.favoriteQueries.pageSize, - pageSizeOptions: ['10', '20', '50'], - total: - dataAnalyzeStore.pageConfigs.favoriteQueries.pageTotal, - pageNo: - dataAnalyzeStore.pageConfigs.favoriteQueries.pageNumber, - onPageNoChange: handleFavoriteQueriesPageNoChange, - onPageSizeChange: handleFavoriteQueriesPageSizeChange - }} - /> - - )} - - - - - ); -}); - -// collpase and expand statement in table -const ExecutionContent: React.FC<{ - type: string; - content: string; - highlightText: string; - algorithmName?: string; -}> = observer(({ type, content, highlightText, algorithmName }) => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { t, i18n } = useTranslation(); - const [isExpand, switchExpand] = useState(dataAnalyzeStore.isSearched.status); - - const statements = - type === 'ALGORITHM' - ? formatAlgorithmStatement(content, algorithmName, t, i18n) - : content.split('\n').filter((statement) => statement !== ''); - - const arrowIconClassName = classnames({ - 'data-analyze-logs-favorite-content-icon': true, - reverse: isExpand - }); - - const handleExpandClick = useCallback(() => { - switchExpand(!isExpand); - }, [isExpand]); - - if (statements.length <= 1) { - return ( - - ); - } - - const statementElement = statements.map((statement, index) => ( -
    - -
    - )); - - return ( -
    - {t('addition.operate.expand-collapse')} -
    - {isExpand ? statementElement : statementElement[0]} -
    -
    - ); -}); - -export interface DeleteConfirmProps { - id: number; - handlePop: (flag: boolean) => void; -} - -export const DeleteConfirm: React.FC = observer( - ({ id, handlePop }) => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { t } = useTranslation(); - const handleDelete = useCallback(async () => { - await dataAnalyzeStore.deleteQueryCollection(id); - - if (dataAnalyzeStore.requestStatus.editQueryCollection === 'failed') { - Message.error({ - content: dataAnalyzeStore.errorInfo.editQueryCollection.message, - size: 'medium', - showCloseIcon: false - }); - } - - dataAnalyzeStore.fetchFavoriteQueries(); - handlePop(false); - }, [dataAnalyzeStore, handlePop, id]); - - const handleCancel = useCallback(() => { - handlePop(false); - }, [handlePop]); - - return ( -
    -
    - {t('addition.common.del-comfirm')} - {t('addition.operate.favorite-del-desc')} -
    - - -
    -
    -
    - ); - } -); - -export default ExecLogAndQueryCollections; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/GremlinKeyWords.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/GremlinKeyWords.ts deleted file mode 100644 index 70c08b42c..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/GremlinKeyWords.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -export const keywords = - 'g|V|E|has|open|close|inV|inE|out|outV|outE|label|store|next|addVertex|clazz|' + - 'limit|traversal|withBulk|values|schema|except|ifNotExist|addEdge|addVertex|property|io|' + - 'filter|loops|readGraph|tree|properties|graph|value|bothE|addV|where|hidden|bothV|without' + - 'both|is|path|it|get|from|to|select|otherV|within|inside|outside|withSack'; - -export const buildinFunctions = - 'targetLabel|sourceLabel|indexLabel|indexLabels|edgeLabel|vertexLabel|propertyKey|getPropertyKey|' + - 'getVertexLabel|getEdgeLabel|getIndexLabel|getPropertyKeys|getVertexLabels|getEdgeLabels|getIndexLabels|' + - 'coin|count|coalesce|createIndex|hasLabel|getLabelId|create|build|append|eliminate|remove|rebuildIndex|' + - 'constant|isDirected|desc|inject|profile|simplePath|eq|neq|gt|gte|lt|lte|queryType|indexFields|frequency|' + - 'links|type|in|on|by|checkDataType|checkValue|validValue|secondary|drop|search|makeEdgeLabel|cyclicPath|' + - 'hasKey|match|sack|aggregate|between|baseType|baseValue|indexType|rebuild|choose|aggregate|iterate|lte|dedup|' + - 'identity|groupCount|until|barrier|fold|unfold|schemaId|checkName|makeIndexLabel|makeVertexLabel|makePropertyKey|' + - 'sideEffect|hasNext|toList|toSet|cap|option|branch|choose|repeat|emit|order|mean|withComputer|subgraph|' + - 'getObjectsAtDepth|hasValue|hasNot|hasId|nullableKey|nullableKeys|sortKeys|link|singleTime|multiTimes|' + - 'enableLabelIndex|userdata|checkExist|linkWithLabel|directed|idStrategy|primaryKeys|primaryKey'; - -export const dataTypes = - 'int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|toString|primitive|' + - 'money|real|number|integer|asInt|asText|dataType|cardinality|asText|asInt|asTimestamp|flatMap|valueMap|' + - 'asByte|asBlob|asDouble|asDate|asFloat|asLong|valueSingle|asBoolean|valueList|valueSet|asUuid|null|Infinity|NaN|undefined'; - -export const baseData = [ - ...keywords.split('|'), - ...buildinFunctions.split('|'), - ...dataTypes.split('|') -]; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/QueryAndAlgorithmLibrary.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/QueryAndAlgorithmLibrary.tsx deleted file mode 100644 index af20dabc0..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/QueryAndAlgorithmLibrary.tsx +++ /dev/null @@ -1,633 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useState, - useRef, - useEffect, - useContext, - useCallback -} from 'react'; -import { reaction } from 'mobx'; -import { observer } from 'mobx-react'; -import CodeMirror from 'codemirror'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Button, Tooltip, Alert, Dropdown } from 'hubble-ui'; - -import 'codemirror/lib/codemirror.css'; -import 'react-popper-tooltip/dist/styles.css'; -import 'codemirror/addon/display/placeholder'; -import 'codemirror/mode/sql/sql'; -import 'codemirror/addon/hint/show-hint.css'; -import 'codemirror/addon/hint/show-hint.js'; -import 'codemirror/addon/hint/sql-hint.js'; - -import { Tooltip as CustomTooltip } from '../../common'; -import Favorite from './common/Favorite'; -import { DataAnalyzeStoreContext } from '../../../stores'; -import { useMultiKeyPress } from '../../../hooks'; - -import LoopDetection from './algorithm/LoopDetection'; -import FocusDetection from './algorithm/FocusDetection'; -import ShortestPath from './algorithm/ShortestPath'; -import ShortestPathAll from './algorithm/ShortestPathAll'; -import AllPath from './algorithm/AllPath'; -import ModelSimilarity from './algorithm/ModelSimilarity'; -import NeighborRank from './algorithm/NeighborRank'; -import KStepNeighbor from './algorithm/KStepNeighbor'; -import KHop from './algorithm/KHop'; -import CustomPath from './algorithm/CustomPath'; -import RadiographicInspection from './algorithm/RadiographicInspection'; -import SameNeighbor from './algorithm/SameNeighbor'; -import WeightedShortestPath from './algorithm/WeightedShortestPath'; -import SingleSourceWeightedShortestPath from './algorithm/SingleSourceWeightedShortestPath'; -import Jaccard from './algorithm/Jaccard'; -import PersonalRank from './algorithm/PersonalRank'; - -import ArrowIcon from '../../../assets/imgs/ic_arrow_16.svg'; -import QuestionMarkIcon from '../../../assets/imgs/ic_question_mark.svg'; -import { Algorithm } from '../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { baseData } from './GremlinKeyWords'; - -export const styles = { - primaryButton: { - width: 72, - marginLeft: 12 - }, - alert: { - margin: '16px 0' - } -}; -const codeRegexp = /[A-Za-z0-9]+/; - -const QueryAndAlgorithmLibrary: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { algorithmAnalyzerStore } = dataAnalyzeStore; - const { t } = useTranslation(); - - const handleTabChange = (tab: string) => () => { - dataAnalyzeStore.resetSwitchTabState(); - - if (tab !== dataAnalyzeStore.currentTab) { - // reset codeEditor value - dataAnalyzeStore.mutateCodeEditorText(''); - - // reset default selection of edge labels - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'labels', - dataAnalyzeStore.edgeTypes.map(({ name }) => name), - 0 - ); - } - - dataAnalyzeStore.setCurrentTab(tab); - // need manually set codeMirror text value to empty here - dataAnalyzeStore.codeEditorInstance?.setValue(''); - // reset algorithm tab to list - algorithmAnalyzerStore.changeCurrentAlgorithm(''); - algorithmAnalyzerStore.switchCollapse(false); - }; - - return ( - <> -
    -
    - {t('data-analyze.category.gremlin-analyze')} -
    -
    - {t('data-analyze.category.algorithm-analyze')} -
    -
    - {dataAnalyzeStore.currentTab === 'gremlin-analyze' && } - {dataAnalyzeStore.currentTab === 'algorithm-analyze' && ( - - )} - - ); -}); - -export const GremlinQuery: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const [isFavoritePop, switchFavoritePop] = useState(false); - const [isCodeExpand, switchCodeExpand] = useState(true); - const codeContainer = useRef(null); - const codeEditor = useRef(); - const keyPressed = useMultiKeyPress(); - const { t } = useTranslation(); - const isDisabledExec = - dataAnalyzeStore.codeEditorText.length === 0 || - !codeRegexp.test(dataAnalyzeStore.codeEditorText) || - dataAnalyzeStore.requestStatus.fetchGraphs === 'pending'; - - const isQueryShortcut = () => { - const isMacOS = navigator.platform.includes('Mac'); - - if (isMacOS) { - return keyPressed.has('MetaLeft') || keyPressed.has('MetaRight'); - } else { - return ( - keyPressed.has('Control') || - keyPressed.has('ControlLeft') || - keyPressed.has('ControlRight') - ); - } - }; - - const handleCodeExpandChange = useCallback( - (flag: boolean) => () => { - switchCodeExpand(flag); - }, - [] - ); - - const handleQueryExecution = useCallback(async () => { - if (codeEditor.current) { - if (dataAnalyzeStore.queryMode === 'query') { - // graph reload - dataAnalyzeStore.switchGraphLoaded(false); - // remove graph data filter board - dataAnalyzeStore.switchShowFilterBoard(false); - dataAnalyzeStore.clearFilteredGraphQueryOptions(); - // forbid edit when exec a query - codeEditor.current.setOption('readOnly', 'nocursor'); - // add temp log into exec log - const timerId = dataAnalyzeStore.addTempExecLog(); - - await dataAnalyzeStore.fetchGraphs(); - codeEditor.current.setOption('readOnly', false); - - // fetch execution logs after query - await dataAnalyzeStore.fetchExecutionLogs(); - // clear timer after fetching new exec logs - window.clearTimeout(timerId); - } else { - await dataAnalyzeStore.createAsyncTask(); - dataAnalyzeStore.fetchExecutionLogs(); - } - } - }, [dataAnalyzeStore]); - - const resetCodeEditorText = useCallback(() => { - switchFavoritePop(false); - dataAnalyzeStore.resetFavoriteRequestStatus('add'); - - if (codeEditor.current) { - codeEditor.current.setValue(''); - dataAnalyzeStore.mutateCodeEditorText(''); - } - }, [dataAnalyzeStore]); - - const codeEditWrapperClassName = classnames({ - 'query-tab-code-edit': true, - hide: !isCodeExpand, - isLoading: dataAnalyzeStore.requestStatus.fetchGraphs === 'pending' - }); - - const handleShowHint = (cm: any) => { - var cursor = cm.getCursor(), - line = cm.getLine(cursor.line); - var start = cursor.ch, - end = cursor.ch; - while (start && /\w/.test(line.charAt(start - 1))) --start; - while (end < line.length && /\w/.test(line.charAt(end))) ++end; - var word = line.slice(start, end).toLowerCase(); - const list = baseData.filter(function (el) { - return el.toLowerCase().indexOf(word) > -1; - }); - return { - list: list, - from: { ch: start, line: cursor.line }, - to: { ch: end, line: cursor.line } - }; - }; - useEffect(() => { - codeEditor.current = CodeMirror.fromTextArea( - codeContainer.current as HTMLTextAreaElement, - { - lineNumbers: true, - lineWrapping: true, - mode: { name: 'text/x-mysql' }, - extraKeys: { Ctrl: 'autocomplete' }, - placeholder: t('addition.operate.input-query-statement'), - hintOptions: { - completeSingle: false, - hint: handleShowHint - } - } - ); - - if (codeEditor.current) { - const handleCodeEditorChange = () => { - dataAnalyzeStore.mutateCodeEditorText( - (codeEditor.current as CodeMirror.Editor).getValue() - ); - }; - - dataAnalyzeStore.assignCodeEditorInstance(codeEditor.current); - codeEditor.current.on('change', handleCodeEditorChange); - - reaction( - () => dataAnalyzeStore.currentId, - () => { - (codeEditor.current as CodeMirror.Editor).setValue(''); - } - ); - - return () => { - (codeEditor.current as CodeMirror.Editor).off( - 'change', - handleCodeEditorChange - ); - }; - } - }, [dataAnalyzeStore]); - - // weird, mobx@reaction is not working when puluse changed, setValue() - // has no influence - useEffect(() => { - if (codeEditor?.current && dataAnalyzeStore.codeEditorText !== '') { - (codeEditor.current as CodeMirror.Editor).setValue( - dataAnalyzeStore.codeEditorText - ); - } - }, [dataAnalyzeStore.pulse]); - // }, [dataAnalyzeStore.pulse, codeEditor?.current]); - - useEffect(() => { - if ( - keyPressed.has('Tab') && - codeEditor.current && - // disable shortcut when drawer pops - !dataAnalyzeStore.isShowGraphInfo && - dataAnalyzeStore.dynamicAddGraphDataStatus === '' - ) { - codeEditor.current.focus(); - } - - if ( - keyPressed.size === 2 && - keyPressed.has('Enter') && - !isDisabledExec && - // disable shortcut when drawer pops - !dataAnalyzeStore.isShowGraphInfo && - dataAnalyzeStore.dynamicAddGraphDataStatus === '' - ) { - if (isQueryShortcut()) { - handleQueryExecution(); - } - } - }, [keyPressed]); - - return ( -
    -
    - -
    - -
    -
    - - {isCodeExpand ? ( -
    - {t('addition.operate.expand')} - {t('addition.operate.collapse')} -
    - ) : ( -
    -
    - {t('addition.operate.collapse')} - {t('addition.operate.expand')} -
    -
    - )} -
    - - {isCodeExpand && - dataAnalyzeStore.requestStatus.fetchGraphs === 'failed' && - dataAnalyzeStore.errorInfo.fetchGraphs.code === 460 && ( - - )} - - {isCodeExpand && ( -
    - - { - dataAnalyzeStore.setQueryMode(e.key); - }} - overlayClassName="improve-zindex" - onClickButton={handleQueryExecution} - size="small" - type="primary" - disabled={ - dataAnalyzeStore.codeEditorText.length === 0 || - !codeRegexp.test(dataAnalyzeStore.codeEditorText) || - dataAnalyzeStore.requestStatus.fetchGraphs === 'pending' - } - /> - - -
    - tips -
    -
    - {dataAnalyzeStore.codeEditorText.length !== 0 ? ( - } - childrenWrapperElement="div" - > - - - ) : ( - - - - )} - -
    - )} -
    - ); -}); - -export const AlgorithmQuery: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { algorithmAnalyzerStore } = dataAnalyzeStore; - const { t } = useTranslation(); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateShortestPathParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.shortestPathAlgorithmParams.source !== '' && - algorithmAnalyzerStore.shortestPathAlgorithmParams.target !== '' && - algorithmAnalyzerStore.shortestPathAlgorithmParams.max_depth !== ''; - - const handleChangeAlgorithm = (algorithm: string) => () => { - algorithmAnalyzerStore.changeCurrentAlgorithm(algorithm); - }; - - const handleExpandClick = () => { - algorithmAnalyzerStore.switchCollapse(!algorithmAnalyzerStore.isCollapse); - }; - - const renderForms = () => { - switch (algorithmAnalyzerStore.currentAlgorithm) { - case Algorithm.loopDetection: - return ; - case Algorithm.focusDetection: - return ; - case Algorithm.shortestPath: - return ; - case Algorithm.shortestPathAll: - return ; - case Algorithm.allPath: - return ; - case Algorithm.modelSimilarity: - return ; - case Algorithm.neighborRank: - return ; - case Algorithm.kStepNeighbor: - return ; - case Algorithm.kHop: - return ; - case Algorithm.customPath: - return ; - case Algorithm.radiographicInspection: - return ; - case Algorithm.sameNeighbor: - return ; - case Algorithm.weightedShortestPath: - return ; - case Algorithm.singleSourceWeightedShortestPath: - return ; - case Algorithm.jaccard: - return ; - case Algorithm.personalRankRecommendation: - return ; - } - }; - - useEffect(() => { - return () => { - algorithmAnalyzerStore.dispose(); - }; - }, []); - - return ( -
    -
    - {algorithmAnalyzerStore.currentAlgorithm === '' ? ( - {t('data-analyze.algorithm-list.title')} - ) : ( -
    - go-back { - algorithmAnalyzerStore.switchCollapse(false); - algorithmAnalyzerStore.changeCurrentAlgorithm(''); - }} - /> - - {t( - `data-analyze.algorithm-list.${algorithmAnalyzerStore.currentAlgorithm}` - )} - -
    - )} -
    - expand-collpase -
    - {algorithmAnalyzerStore.isCollapse ? null : algorithmAnalyzerStore.currentAlgorithm === - '' ? ( - <> -
    - {[ - Algorithm.loopDetection, - Algorithm.focusDetection, - Algorithm.shortestPath, - Algorithm.shortestPathAll, - Algorithm.allPath - ].map((algorithm) => ( - - {t(`data-analyze.algorithm-list.${algorithm}`)} - - ))} -
    -
    - {[ - Algorithm.modelSimilarity, - Algorithm.neighborRank, - Algorithm.kStepNeighbor, - Algorithm.kHop, - Algorithm.customPath - ].map((algorithm) => ( - - {t(`data-analyze.algorithm-list.${algorithm}`)} - - ))} -
    -
    - {[ - Algorithm.radiographicInspection, - Algorithm.sameNeighbor, - Algorithm.weightedShortestPath, - Algorithm.singleSourceWeightedShortestPath, - Algorithm.jaccard - ].map((algorithm) => ( - - {t(`data-analyze.algorithm-list.${algorithm}`)} - - ))} -
    -
    - {[Algorithm.personalRankRecommendation].map((algorithm) => ( - - {t(`data-analyze.algorithm-list.${algorithm}`)} - - ))} -
    - - ) : ( - renderForms() - )} -
    - ); -}); - -export default QueryAndAlgorithmLibrary; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/AllPath.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/AllPath.tsx deleted file mode 100644 index 8695bb8cf..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/AllPath.tsx +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { GraphManagementStoreContext } from '../../../../stores'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; - -const AllPath = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateAllPathParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.allPathParams.source !== '' && - algorithmAnalyzerStore.allPathParams.target !== '' && - algorithmAnalyzerStore.allPathParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.all-path.options.source')} - -
    - { - algorithmAnalyzerStore.mutateAllPathParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateAllPathParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateAllPathParams('source'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.all-path.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.all-path.options.target')} - -
    - { - algorithmAnalyzerStore.mutateAllPathParams( - 'target', - e.value as string - ); - - algorithmAnalyzerStore.validateAllPathParams('target'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateAllPathParams('target'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.all-path.options.max_degree')} - - -
    - { - algorithmAnalyzerStore.mutateAllPathParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateAllPathParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateAllPathParams('max_degree'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.all-path.options.direction')} - -
    - ) => { - algorithmAnalyzerStore.mutateAllPathParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t('data-analyze.algorithm-forms.all-path.options.capacity')} - -
    - { - algorithmAnalyzerStore.mutateAllPathParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateAllPathParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateAllPathParams('capacity'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.all-path.options.max_depth')} - - -
    - { - algorithmAnalyzerStore.mutateAllPathParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateAllPathParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateAllPathParams('max_depth'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.all-path.options.limit')} - -
    - { - algorithmAnalyzerStore.mutateAllPathParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateAllPathParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateAllPathParams('limit'); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default AllPath; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/CustomPath.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/CustomPath.tsx deleted file mode 100644 index e57b2ed59..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/CustomPath.tsx +++ /dev/null @@ -1,1115 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { - size, - flatten, - flattenDeep, - uniq, - isEmpty, - cloneDeep, - fromPairs -} from 'lodash-es'; -import { Button, Radio, Input, Select } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { isDataTypeNumeric, calcAlgorithmFormWidth } from '../../../../utils'; - -const CustomPath = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const { t } = useTranslation(); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 390 - ); - - const formWidthInStep = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 310, - 380 - ); - - const formSmallWidthInStep = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 150, - 160 - ); - - const sourceType = algorithmAnalyzerStore.customPathParams.method; - - const allowAddNewProperty = !flatten( - algorithmAnalyzerStore.customPathParams.vertexProperty - ).some((value) => value === ''); - - const addNewPropertyClassName = - allowAddNewProperty && - dataAnalyzeStore.requestStatus.fetchGraphs !== 'pending' - ? 'query-tab-content-internal-expand-manipulation-enable' - : 'query-tab-content-internal-expand-manipulation-disabled'; - - const allowAddNewRuleProperty = !flattenDeep( - algorithmAnalyzerStore.customPathParams.steps.map( - ({ properties }) => properties - ) - ).some((value) => value === ''); - - const addNewRulePropertyClassName = - allowAddNewRuleProperty && - dataAnalyzeStore.requestStatus.fetchGraphs !== 'pending' - ? 'query-tab-content-internal-expand-manipulation-enable' - : 'query-tab-content-internal-expand-manipulation-disabled'; - - const isValidSourceId = - algorithmAnalyzerStore.customPathParams.method === 'id' && - algorithmAnalyzerStore.customPathParams.source !== ''; - - const isValidProperty = - algorithmAnalyzerStore.customPathParams.method === 'property' && - !( - isEmpty(algorithmAnalyzerStore.customPathParams.vertexType) && - flatten( - algorithmAnalyzerStore.customPathParams.vertexProperty - ).every((value) => isEmpty(value)) - ) && - algorithmAnalyzerStore.customPathParams.vertexProperty.every( - ([key, value]) => (!isEmpty(key) ? !isEmpty(value) : true) - ); - - const isValidateRuleProperties = algorithmAnalyzerStore.customPathParams.steps.every( - ({ properties }) => { - if (size(properties) === 1) { - return ( - (isEmpty(properties[0][0]) && isEmpty(properties[0][1])) || - (!isEmpty(properties[0][0]) && !isEmpty(properties[0][1])) - ); - } else { - return properties.every(([, value]) => !isEmpty(value)); - } - } - ); - - const isValidRuleWeight = algorithmAnalyzerStore.customPathParams.steps.every( - ({ weight_by, default_weight }) => - algorithmAnalyzerStore.customPathParams.sort_by === 'NONE' || - (weight_by === '__CUSTOM_WEIGHT__' && default_weight !== '') || - (weight_by !== '__CUSTOM_WEIGHT__' && !isEmpty(weight_by)) - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateCustomPathParmasErrorMessage - ).every((value) => Array.isArray(value) || value === '') && - algorithmAnalyzerStore.validateCustomPathParmasErrorMessage.steps.every( - (step) => Object.values(step).every((value) => value === '') - ) && - (algorithmAnalyzerStore.customPathParams.method === 'id' - ? algorithmAnalyzerStore.customPathParams.source !== '' - : true) && - (isValidSourceId || isValidProperty) && - isValidateRuleProperties && - isValidRuleWeight; - - const isValidAddRule = algorithmAnalyzerStore.validateCustomPathParmasErrorMessage.steps.every( - (step) => Object.values(step).every((value) => value === '') - ); - - return ( -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.custom-path.options.method')} - -
    - ) => { - algorithmAnalyzerStore.switchCustomPathMethod(e.target.value); - }} - > - - {t( - 'data-analyze.algorithm-forms.custom-path.radio-value.specific-id' - )} - - - {t( - 'data-analyze.algorithm-forms.custom-path.radio-value.filtered-type-property' - )} - - -
    -
    - - {sourceType === 'id' && ( -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.custom-path.options.source')} - -
    - - { - algorithmAnalyzerStore.mutateCustomPathParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateCustomPathParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateCustomPathParams('source'); - } - }} - /> -
    -
    - )} - - {sourceType !== 'id' && ( - <> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.custom-path.options.vertex-type' - )} - -
    - -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.custom-path.options.vertex-property' - )} - -
    - -
    - {algorithmAnalyzerStore.customPathParams.vertexProperty.map( - ([key, value], propertyIndex) => { - const currentVertexType = dataAnalyzeStore.vertexTypes.find( - ({ name }) => - name === - algorithmAnalyzerStore.customPathParams.vertexType - ); - - return ( -
    - -
    - { - const vertexProperty = cloneDeep( - algorithmAnalyzerStore.customPathParams - .vertexProperty - ); - - vertexProperty[propertyIndex][1] = e.value; - - algorithmAnalyzerStore.mutateCustomPathParams( - 'vertexProperty', - vertexProperty - ); - - algorithmAnalyzerStore.validateCustomPathParams( - 'vertexProperty' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateCustomPathParams( - 'vertexProperty' - ); - } - }} - /> -
    - {size( - algorithmAnalyzerStore.customPathParams - .vertexProperty - ) > 1 && ( -
    { - algorithmAnalyzerStore.removeCustomPathVertexProperty( - propertyIndex - ); - }} - > - {t( - 'data-analyze.algorithm-forms.custom-path.delete' - )} -
    - )} -
    - ); - } - )} -
    -
    -
    - {algorithmAnalyzerStore.customPathParams.vertexType === '' && - !allowAddNewProperty && - size(algorithmAnalyzerStore.customPathParams.vertexProperty) === - 1 ? ( -
    - - {t( - 'data-analyze.algorithm-forms.custom-path.hint.vertex_type_or_property' - )} - -
    - ) : ( -
    - { - if ( - allowAddNewProperty && - dataAnalyzeStore.requestStatus.fetchGraphs !== 'pending' - ) { - algorithmAnalyzerStore.addCustomPathVertexProperty(); - } - }} - > - {t('data-analyze.algorithm-forms.custom-path.add')} - -
    - )} - - )} - -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.custom-path.options.sort_by')} - -
    - ) => { - algorithmAnalyzerStore.mutateCustomPathParams( - 'sort_by', - e.target.value - ); - - if (e.target.value === 'NONE') { - algorithmAnalyzerStore.customPathParams.steps.forEach( - (_, ruleIndex) => { - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'weight_by', - '', - ruleIndex - ); - - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'default_weight', - '', - ruleIndex - ); - } - ); - } - }} - > - - {t('data-analyze.algorithm-forms.custom-path.radio-value.none')} - - - {t( - 'data-analyze.algorithm-forms.custom-path.radio-value.ascend' - )} - - - {t( - 'data-analyze.algorithm-forms.custom-path.radio-value.descend' - )} - - -
    -
    -
    -
    -
    - - {t('data-analyze.algorithm-forms.custom-path.options.capacity')} - -
    - { - algorithmAnalyzerStore.mutateCustomPathParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateCustomPathParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateCustomPathParams('capacity'); - } - }} - /> -
    -
    -
    -
    -
    - - {t('data-analyze.algorithm-forms.custom-path.options.limit')} - -
    - { - algorithmAnalyzerStore.mutateCustomPathParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateCustomPathParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateCustomPathParams('limit'); - } - }} - /> -
    -
    -
    - - -
    -
    - -
    - {algorithmAnalyzerStore.customPathParams.steps.map( - ( - { - uuid, - direction, - labels, - degree, - sample, - properties, - weight_by, - default_weight - }, - ruleIndex - ) => { - return ( -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.custom-path.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'direction', - e.target.value, - ruleIndex - ); - }} - > - both - out - in - - {size(algorithmAnalyzerStore.customPathParams.steps) > 1 && ( -
    { - algorithmAnalyzerStore.removeCustomPathRule(ruleIndex); - }} - > - 删除 -
    - )} -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.custom-path.options.labels' - )} - -
    - -
    - -
    -
    - - {t( - 'data-analyze.algorithm-forms.custom-path.options.properties' - )} - -
    -
    - <> - {properties.map(([key, value], propertyIndex) => { - return ( -
    -
    - -
    - -
    - { - const clonedRuleProperties = cloneDeep( - properties - ); - - clonedRuleProperties[propertyIndex][1] = - e.value; - - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'properties', - clonedRuleProperties, - ruleIndex - ); - }} - /> -
    - - {size(properties) > 1 && ( -
    { - algorithmAnalyzerStore.removeCustomPathRuleProperty( - ruleIndex, - propertyIndex - ); - }} - > - {t( - 'data-analyze.algorithm-forms.custom-path.delete' - )} -
    - )} -
    - ); - })} - -
    - { - if ( - allowAddNewRuleProperty && - dataAnalyzeStore.requestStatus.fetchGraphs !== - 'pending' - ) { - algorithmAnalyzerStore.addCustomPathRuleProperty( - ruleIndex - ); - } - }} - > - {t('data-analyze.algorithm-forms.custom-path.add')} - -
    - -
    -
    - - {algorithmAnalyzerStore.customPathParams.sort_by !== 'NONE' && ( -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.custom-path.options.weight_by' - )} - -
    - -
    - )} - {algorithmAnalyzerStore.customPathParams.steps[ruleIndex] - .weight_by === '__CUSTOM_WEIGHT__' && ( -
    -
    - -
    - { - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'default_weight', - e.value as string, - ruleIndex - ); - - algorithmAnalyzerStore.validateCustomPathRules( - 'default_weight', - ruleIndex - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateCustomPathRules( - 'default_weight', - ruleIndex - ); - } - }} - /> -
    - )} -
    -
    - - {t( - 'data-analyze.algorithm-forms.custom-path.options.degree' - )} - -
    - { - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'degree', - e.value as string, - ruleIndex - ); - - algorithmAnalyzerStore.validateCustomPathRules( - 'degree', - ruleIndex - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateCustomPathRules( - 'degree', - ruleIndex - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.custom-path.options.sample' - )} - -
    - { - algorithmAnalyzerStore.mutateCustomPathRuleParams( - 'sample', - e.value as string, - ruleIndex - ); - - algorithmAnalyzerStore.validateCustomPathRules( - 'sample', - ruleIndex - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateCustomPathRules( - 'sample', - ruleIndex - ); - } - }} - /> -
    -
    - ); - } - )} -
    - { - if (isValidAddRule) { - algorithmAnalyzerStore.addCustomPathRule(); - } - }} - > - {t('data-analyze.algorithm-forms.custom-path.add-new-rule')} - -
    -
    -
    - ); -}); - -export default CustomPath; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/FocusDetection.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/FocusDetection.tsx deleted file mode 100644 index 58a37782c..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/FocusDetection.tsx +++ /dev/null @@ -1,423 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, createContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { GraphManagementStoreContext } from '../../../../stores'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -const FocusDetection = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateFocusDetectionParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.focusDetectionParams.source !== '' && - algorithmAnalyzerStore.focusDetectionParams.target !== '' && - algorithmAnalyzerStore.focusDetectionParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.focus-detection.options.source')} - -
    - { - algorithmAnalyzerStore.mutateFocusDetectionParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateFocusDetectionParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateFocusDetectionParams('source'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.focus-detection.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.focus-detection.options.target')} - -
    - { - algorithmAnalyzerStore.mutateFocusDetectionParams( - 'target', - e.value as string - ); - - algorithmAnalyzerStore.validateFocusDetectionParams('target'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateFocusDetectionParams('target'); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.focus-detection.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateFocusDetectionParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateFocusDetectionParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateFocusDetectionParams( - 'max_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.focus-detection.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateFocusDetectionParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.focus-detection.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateFocusDetectionParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateFocusDetectionParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateFocusDetectionParams('capacity'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.focus-detection.options.max_depth' - )} - - -
    - { - algorithmAnalyzerStore.mutateFocusDetectionParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateFocusDetectionParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateFocusDetectionParams( - 'max_depth' - ); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.focus-detection.options.limit')} - -
    - { - algorithmAnalyzerStore.mutateFocusDetectionParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateFocusDetectionParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateFocusDetectionParams('limit'); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default FocusDetection; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/Jaccard.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/Jaccard.tsx deleted file mode 100644 index 8dcb148f4..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/Jaccard.tsx +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const Jaccard = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const { t } = useTranslation(); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateJaccardParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.jaccardParams.vertex !== '' && - algorithmAnalyzerStore.jaccardParams.other !== ''; - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.jaccard.options.vertex')} - -
    - { - algorithmAnalyzerStore.mutateJaccardParams( - 'vertex', - e.value as string - ); - - algorithmAnalyzerStore.validateJaccardParams('vertex'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateJaccardParams('vertex'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.jaccard.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.jaccard.options.other')} - -
    - { - algorithmAnalyzerStore.mutateJaccardParams( - 'other', - e.value as string - ); - - algorithmAnalyzerStore.validateJaccardParams('other'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateJaccardParams('other'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.jaccard.options.max_degree')} - - -
    - { - algorithmAnalyzerStore.mutateJaccardParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateJaccardParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateJaccardParams('max_degree'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.jaccard.options.direction')} - -
    - ) => { - algorithmAnalyzerStore.mutateJaccardParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - -
    -
    - ); -}); - -export default Jaccard; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/KHop.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/KHop.tsx deleted file mode 100644 index bd12e6f83..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/KHop.tsx +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; -import { styles } from '../QueryAndAlgorithmLibrary'; - -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const KHop = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values(algorithmAnalyzerStore.validateKHopParamsErrorMessage).every( - (value) => value === '' - ) && - algorithmAnalyzerStore.kHopParams.source !== '' && - algorithmAnalyzerStore.kHopParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.k-hop.options.source')} - -
    - { - algorithmAnalyzerStore.mutateKHopParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateKHopParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKHopParams('source'); - } - }} - /> -
    -
    -
    - {t('data-analyze.algorithm-forms.k-hop.options.label')} -
    - -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.k-hop.options.direction')} - -
    - ) => { - algorithmAnalyzerStore.mutateKHopParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t('data-analyze.algorithm-forms.k-hop.options.max_degree')} - - -
    - { - algorithmAnalyzerStore.mutateKHopParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateKHopParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKHopParams('max_degree'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.k-hop.options.max_depth')} - - -
    - { - algorithmAnalyzerStore.mutateKHopParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateKHopParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKHopParams('max_depth'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.k-hop.options.capacity')} - -
    - { - algorithmAnalyzerStore.mutateKHopParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateKHopParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKHopParams('capacity'); - } - }} - /> -
    -
    -
    -
    -
    - - {t('data-analyze.algorithm-forms.k-hop.options.nearest')} - - -
    - { - algorithmAnalyzerStore.mutateKHopParams('nearest', checked); - }} - /> -
    -
    -
    - {t('data-analyze.algorithm-forms.k-hop.options.limit')} -
    - { - algorithmAnalyzerStore.mutateKHopParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateKHopParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKHopParams('limit'); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default KHop; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/KStepNeighbor.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/KStepNeighbor.tsx deleted file mode 100644 index 6b46cd94a..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/KStepNeighbor.tsx +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const KStepNeighbor = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateKStepNeighborParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.kStepNeighborParams.source !== '' && - algorithmAnalyzerStore.kStepNeighborParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.k-step-neighbor.options.source')} - -
    - { - algorithmAnalyzerStore.mutateKStepNeighborParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateKStepNeighborParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKStepNeighborParams('source'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.k-step-neighbor.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.k-step-neighbor.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateKStepNeighborParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.k-step-neighbor.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateKStepNeighborParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateKStepNeighborParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKStepNeighborParams( - 'max_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.k-step-neighbor.options.max_depth' - )} - - -
    - { - algorithmAnalyzerStore.mutateKStepNeighborParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateKStepNeighborParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKStepNeighborParams('max_depth'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.k-step-neighbor.options.limit')} - -
    - { - algorithmAnalyzerStore.mutateKStepNeighborParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateKStepNeighborParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateKStepNeighborParams('limit'); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default KStepNeighbor; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/LoopDetection.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/LoopDetection.tsx deleted file mode 100644 index 49b134db7..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/LoopDetection.tsx +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { GraphManagementStoreContext } from '../../../../stores'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const LoopDetection = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateLoopDetectionParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.loopDetectionParams.source !== '' && - algorithmAnalyzerStore.loopDetectionParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.loop-detection.options.source')} - -
    - { - algorithmAnalyzerStore.mutateLoopDetectionParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateLoopDetectionParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateLoopDetectionParams('source'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.loop-detection.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.loop-detection.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateLoopDetectionParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.loop-detection.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateLoopDetectionParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateLoopDetectionParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateLoopDetectionParams( - 'max_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.loop-detection.options.max_depth' - )} - - -
    - { - algorithmAnalyzerStore.mutateLoopDetectionParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateLoopDetectionParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateLoopDetectionParams('max_depth'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.loop-detection.options.limit')} - -
    - { - algorithmAnalyzerStore.mutateLoopDetectionParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateLoopDetectionParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateLoopDetectionParams('limit'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.loop-detection.options.source_in_ring' - )} - -
    - { - algorithmAnalyzerStore.mutateLoopDetectionParams( - 'source_in_ring', - checked - ); - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.loop-detection.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateLoopDetectionParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateLoopDetectionParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateLoopDetectionParams('capacity'); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default LoopDetection; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ModelSimilarity.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ModelSimilarity.tsx deleted file mode 100644 index 4ad2ed7ab..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ModelSimilarity.tsx +++ /dev/null @@ -1,1166 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { flatten, cloneDeep, size, isEmpty } from 'lodash-es'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; - -const ModelSimilarity = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 390 - ); - - const sourceType = algorithmAnalyzerStore.modelSimilarityParams.method; - - const allowAddNewProperty = !flatten( - algorithmAnalyzerStore.modelSimilarityParams.vertexProperty - ).some((value) => value === ''); - - const addNewPropertyClassName = - allowAddNewProperty && - dataAnalyzeStore.requestStatus.fetchGraphs !== 'pending' - ? 'query-tab-content-internal-expand-manipulation-enable' - : 'query-tab-content-internal-expand-manipulation-disabled'; - - const isValidSourceId = - algorithmAnalyzerStore.modelSimilarityParams.method === 'id' && - algorithmAnalyzerStore.modelSimilarityParams.source !== ''; - - const isValidProperty = - algorithmAnalyzerStore.modelSimilarityParams.method === 'property' && - !( - isEmpty(algorithmAnalyzerStore.modelSimilarityParams.vertexType) && - flatten( - algorithmAnalyzerStore.modelSimilarityParams.vertexProperty - ).every((value) => isEmpty(value)) - ) && - algorithmAnalyzerStore.modelSimilarityParams.vertexProperty.every( - ([key, value]) => (!isEmpty(key) ? !isEmpty(value) : true) - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateModelSimilartiyParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.modelSimilarityParams.least_neighbor !== '' && - algorithmAnalyzerStore.modelSimilarityParams.similarity !== '' && - (!isEmpty(algorithmAnalyzerStore.modelSimilarityParams.property_filter) - ? algorithmAnalyzerStore.modelSimilarityParams.least_property_number !== - '' - : true) && - (isValidSourceId || isValidProperty); - - return ( -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.method' - )} - -
    - ) => { - algorithmAnalyzerStore.switchModelSimilarityMethod( - e.target.value - ); - }} - > - - {t( - 'data-analyze.algorithm-forms.model-similarity.radio-value.specific-id' - )} - - - {t( - 'data-analyze.algorithm-forms.model-similarity.radio-value.filtered-type-property' - )} - - -
    -
    - - {sourceType === 'id' && ( -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.source' - )} - -
    - - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'source' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'source' - ); - } - }} - /> -
    -
    - )} - - {sourceType !== 'id' && ( - <> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.vertex-type' - )} - -
    - -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.vertex-property' - )} - -
    - -
    - {algorithmAnalyzerStore.modelSimilarityParams.vertexProperty.map( - ([key, value], propertyIndex) => { - const currentVertexType = dataAnalyzeStore.vertexTypes.find( - ({ name }) => - name === - algorithmAnalyzerStore.modelSimilarityParams - .vertexType - ); - - return ( -
    - -
    - { - const vertexProperty = cloneDeep( - algorithmAnalyzerStore.modelSimilarityParams - .vertexProperty - ); - - vertexProperty[propertyIndex][1] = e.value; - - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'vertexProperty', - vertexProperty - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'vertexProperty' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'vertexProperty' - ); - } - }} - /> -
    - {size( - algorithmAnalyzerStore.modelSimilarityParams - .vertexProperty - ) > 1 && ( -
    { - algorithmAnalyzerStore.removeModelSimilarityVertexProperty( - propertyIndex - ); - }} - > - {t( - 'data-analyze.algorithm-forms.model-similarity.delete' - )} -
    - )} -
    - ); - } - )} -
    -
    -
    - {algorithmAnalyzerStore.modelSimilarityParams.vertexType === '' && - !allowAddNewProperty && - size( - algorithmAnalyzerStore.modelSimilarityParams.vertexProperty - ) === 1 ? ( -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.hint.vertex_type_or_property' - )} - -
    - ) : ( -
    - { - if ( - allowAddNewProperty && - dataAnalyzeStore.requestStatus.fetchGraphs !== 'pending' - ) { - algorithmAnalyzerStore.addModelSimilarityVertexProperty(); - } - }} - > - {t('data-analyze.algorithm-forms.model-similarity.add')} - -
    - )} - - )} - -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    - -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.least_neighbor' - )} - - -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'least_neighbor', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'least_neighbor' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'least_neighbor' - ); - } - }} - /> -
    -
    - -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.similarity' - )} - - -
    - - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'similarity', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'similarity' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'similarity' - ); - } - }} - /> -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.label' - )} - -
    - -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.max_similar' - )} - - -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'max_similar', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'max_similar' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'max_similar' - ); - } - }} - /> -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.least_similar' - )} - -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'least_similar', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'least_similar' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'least_similar' - ); - } - }} - /> -
    -
    - -
    - - -
    -
    - -
    -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.property_filter' - )} - -
    - -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.least_property_number' - )} - - -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'least_property_number', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'least_property_number' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'least_property_number' - ); - } - }} - /> -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'max_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'max_degree' - ); - } - }} - /> -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams( - 'capacity' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams( - 'capacity' - ); - } - }} - /> -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.limit' - )} - -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateModelSimilarityParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateModelSimilarityParams('limit'); - } - }} - /> -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.return_common_connection' - )} - - -
    -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'return_common_connection', - checked - ); - }} - /> -
    -
    -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.model-similarity.options.return_complete_info' - )} - -
    -
    - { - algorithmAnalyzerStore.mutateModelSimilarityParams( - 'return_complete_info', - checked - ); - }} - /> -
    -
    -
    -
    -
    - ); -}); - -export default ModelSimilarity; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/NeighborRank.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/NeighborRank.tsx deleted file mode 100644 index 845fd268f..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/NeighborRank.tsx +++ /dev/null @@ -1,500 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { size } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Button, Radio, Input, Select } from 'hubble-ui'; - -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; - -const NeighborRank = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const formWidthInStep = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 310, - 380 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateNeighborRankParamsParamsErrorMessage - ).every((value) => Array.isArray(value) || value === '') && - algorithmAnalyzerStore.validateNeighborRankParamsParamsErrorMessage.steps.every( - (step) => Object.values(step).every((value) => value === '') - ) && - algorithmAnalyzerStore.neighborRankParams.source !== '' && - algorithmAnalyzerStore.neighborRankParams.alpha !== ''; - - const isValidAddRule = algorithmAnalyzerStore.validateNeighborRankParamsParamsErrorMessage.steps.every( - (step) => Object.values(step).every((value) => value === '') - ); - - return ( -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.neighbor-rank.options.source')} - -
    - { - algorithmAnalyzerStore.mutateNeighborRankParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateNeighborRankParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateNeighborRankParams('source'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.neighbor-rank.options.alpha')} - -
    - { - algorithmAnalyzerStore.mutateNeighborRankParams( - 'alpha', - e.value as string - ); - - algorithmAnalyzerStore.validateNeighborRankParams('alpha'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateNeighborRankParams('alpha'); - } - }} - /> -
    -
    -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.neighbor-rank.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateNeighborRankParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateNeighborRankParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateNeighborRankParams('capacity'); - } - }} - /> -
    -
    -
    - - -
    -
    - -
    - {algorithmAnalyzerStore.neighborRankParams.steps.map( - ({ uuid, direction, labels, degree, top }, ruleIndex) => { - return ( -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.neighbor-rank.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateNeighborRankRuleParams( - 'direction', - e.target.value, - ruleIndex - ); - }} - > - both - out - in - - {size(algorithmAnalyzerStore.neighborRankParams.steps) > - 1 && ( -
    { - algorithmAnalyzerStore.removeNeighborRankRule( - ruleIndex - ); - }} - > - {t('addition.common.del')} -
    - )} -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.neighbor-rank.options.label' - )} - -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.neighbor-rank.options.degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateNeighborRankRuleParams( - 'degree', - e.value as string, - ruleIndex - ); - - algorithmAnalyzerStore.validateNeighborRankRules( - 'degree', - ruleIndex - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateNeighborRankRules( - 'degree', - ruleIndex - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.neighbor-rank.options.top' - )} - - -
    - { - algorithmAnalyzerStore.mutateNeighborRankRuleParams( - 'top', - e.value as string, - ruleIndex - ); - - algorithmAnalyzerStore.validateNeighborRankRules( - 'top', - ruleIndex - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateNeighborRankRules( - 'top', - ruleIndex - ); - } - }} - /> -
    -
    - ); - } - )} -
    - { - if (isValidAddRule) { - algorithmAnalyzerStore.addNeighborRankRule(); - } - }} - > - {t('data-analyze.algorithm-forms.neighbor-rank.add-new-rule')} - -
    -
    -
    - ); -}); - -export default NeighborRank; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/PersonalRank.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/PersonalRank.tsx deleted file mode 100644 index 866b8dbfd..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/PersonalRank.tsx +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const PersonalRank = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validatePersonalRankErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.personalRankParams.source !== '' && - algorithmAnalyzerStore.personalRankParams.alpha !== '' && - algorithmAnalyzerStore.personalRankParams.label !== '' && - algorithmAnalyzerStore.personalRankParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.personal-rank.options.source')} - -
    - { - algorithmAnalyzerStore.mutatePersonalRankParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validatePersonalRankParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validatePersonalRankParams('source'); - } - }} - /> -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.personal-rank.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.personal-rank.options.alpha')} - -
    - { - algorithmAnalyzerStore.mutatePersonalRankParams( - 'alpha', - e.value as string - ); - - algorithmAnalyzerStore.validatePersonalRankParams('alpha'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validatePersonalRankParams('alpha'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.personal-rank.options.degree')} - - -
    - { - algorithmAnalyzerStore.mutatePersonalRankParams( - 'degree', - e.value as string - ); - - algorithmAnalyzerStore.validatePersonalRankParams('degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validatePersonalRankParams('degree'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.personal-rank.options.max_depth' - )} - -
    - { - algorithmAnalyzerStore.mutatePersonalRankParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validatePersonalRankParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validatePersonalRankParams('max_depth'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.personal-rank.options.limit')} - -
    - { - algorithmAnalyzerStore.mutatePersonalRankParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validatePersonalRankParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validatePersonalRankParams('limit'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.personal-rank.options.with_label' - )} - - -
    - ) => { - algorithmAnalyzerStore.mutatePersonalRankParams( - 'with_label', - e.target.value - ); - }} - > - - {t( - 'data-analyze.algorithm-forms.personal-rank.with-label-radio-value.same_label' - )} - - - {t( - 'data-analyze.algorithm-forms.personal-rank.with-label-radio-value.other_label' - )} - - - {t( - 'data-analyze.algorithm-forms.personal-rank.with-label-radio-value.both_label' - )} - - -
    -
    -
    - - {t('data-analyze.algorithm-forms.personal-rank.options.sorted')} - - -
    -
    - { - algorithmAnalyzerStore.mutatePersonalRankParams( - 'sorted', - checked - ); - }} - /> -
    -
    -
    -
    - - -
    -
    - ); -}); - -export default PersonalRank; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/RadiographicInspection.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/RadiographicInspection.tsx deleted file mode 100644 index 682424fd3..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/RadiographicInspection.tsx +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const RadiographicInspection = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateRadiographicInspectionParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.radiographicInspectionParams.source !== '' && - algorithmAnalyzerStore.radiographicInspectionParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.radiographic-inspection.options.source' - )} - -
    - { - algorithmAnalyzerStore.mutateRadiographicInspectionParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'source' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'source' - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.radiographic-inspection.options.label' - )} - -
    - -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.radiographic-inspection.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateRadiographicInspectionParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.radiographic-inspection.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateRadiographicInspectionParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'max_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'max_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.radiographic-inspection.options.max_depth' - )} - - -
    - { - algorithmAnalyzerStore.mutateRadiographicInspectionParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'max_depth' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'max_depth' - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.radiographic-inspection.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateRadiographicInspectionParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'capacity' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'capacity' - ); - } - }} - /> -
    -
    -
    -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.radiographic-inspection.options.limit' - )} - -
    - { - algorithmAnalyzerStore.mutateRadiographicInspectionParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'limit' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateRadiographicInspectionParams( - 'limit' - ); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default RadiographicInspection; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/SameNeighbor.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/SameNeighbor.tsx deleted file mode 100644 index 84496448d..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/SameNeighbor.tsx +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const SameNeighbor = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateSameNeighborParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.sameNeighborParams.vertex !== '' && - algorithmAnalyzerStore.sameNeighborParams.other !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.same-neighbor.options.vertex')} - -
    - { - algorithmAnalyzerStore.mutateSameNeighborParams( - 'vertex', - e.value as string - ); - - algorithmAnalyzerStore.validateSameNeighborParams('vertex'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSameNeighborParams('vertex'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.same-neighbor.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.same-neighbor.options.other')} - -
    - { - algorithmAnalyzerStore.mutateSameNeighborParams( - 'other', - e.value as string - ); - - algorithmAnalyzerStore.validateSameNeighborParams('other'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSameNeighborParams('other'); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.same-neighbor.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateSameNeighborParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateSameNeighborParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSameNeighborParams('max_degree'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.same-neighbor.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateSameNeighborParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t('data-analyze.algorithm-forms.same-neighbor.options.limit')} - -
    - { - algorithmAnalyzerStore.mutateSameNeighborParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateSameNeighborParams('limit'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSameNeighborParams('limit'); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default SameNeighbor; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ShortestPath.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ShortestPath.tsx deleted file mode 100644 index 1df1a02ce..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ShortestPath.tsx +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; - -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -const ShortestPath = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateShortestPathParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.shortestPathAlgorithmParams.source !== '' && - algorithmAnalyzerStore.shortestPathAlgorithmParams.target !== '' && - algorithmAnalyzerStore.shortestPathAlgorithmParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.shortest-path.options.source')} - -
    - { - algorithmAnalyzerStore.mutateShortestPathParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathParams('source'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.shortest-path.options.label')} - -
    - -
    -
    -
    -
    -
    - * - - {t('data-analyze.algorithm-forms.shortest-path.options.target')} - -
    - { - algorithmAnalyzerStore.mutateShortestPathParams( - 'target', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathParams('target'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathParams('target'); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.shortest-path.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateShortestPathParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathParams('max_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathParams('max_degree'); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.shortest-path.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateShortestPathParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.shortest-path.options.skip_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateShortestPathParams( - 'skip_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathParams('skip_degree'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathParams( - 'skip_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.shortest-path.options.max_depth' - )} - - -
    - { - algorithmAnalyzerStore.mutateShortestPathParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathParams('max_depth'); - } - }} - /> -
    -
    -
    - - {t('data-analyze.algorithm-forms.shortest-path.options.capacity')} - -
    - { - algorithmAnalyzerStore.mutateShortestPathParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathParams('capacity'); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default ShortestPath; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ShortestPathAll.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ShortestPathAll.tsx deleted file mode 100644 index aa206b120..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/ShortestPathAll.tsx +++ /dev/null @@ -1,469 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, createContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; - -const ShortestPathAll = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 400 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateShortestPathAllParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.shortestPathAllParams.source !== '' && - algorithmAnalyzerStore.shortestPathAllParams.target !== '' && - algorithmAnalyzerStore.shortestPathAllParams.max_depth !== ''; - - return ( -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.source' - )} - -
    - { - algorithmAnalyzerStore.mutateShortestPathAllParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathAllParams('source'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathAllParams('source'); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.label' - )} - -
    - -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.target' - )} - -
    - { - algorithmAnalyzerStore.mutateShortestPathAllParams( - 'target', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathAllParams('target'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathAllParams('target'); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateShortestPathAllParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathAllParams( - 'max_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathAllParams( - 'max_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateShortestPathAllParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateShortestPathAllParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathAllParams('capacity'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathAllParams( - 'capacity' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.max_depth' - )} - - -
    - { - algorithmAnalyzerStore.mutateShortestPathAllParams( - 'max_depth', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathAllParams('max_depth'); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathAllParams( - 'max_depth' - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.shortest-path-all.options.skip_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateShortestPathAllParams( - 'skip_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateShortestPathAllParams( - 'skip_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateShortestPathAllParams( - 'skip_degree' - ); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default ShortestPathAll; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/SingleSourceWeightedShortestPath.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/SingleSourceWeightedShortestPath.tsx deleted file mode 100644 index 3a84bf16b..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/SingleSourceWeightedShortestPath.tsx +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; - -import { GraphManagementStoreContext } from '../../../../stores'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { isDataTypeNumeric, calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const SingleSourceWeightedShortestPath = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 340, - 380 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.singleSourceWeightedShortestPathParams.source !== ''; - - return ( -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.source' - )} - -
    - { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'source' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'source' - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.label' - )} - -
    - -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'max_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'max_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.weight' - )} - -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.skip_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - 'skip_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'skip_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'skip_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.with_vertex' - )} - -
    - { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - 'with_vertex', - checked - ); - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'capacity' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'capacity' - ); - } - }} - /> -
    -
    -
    -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.single-source-weighted-shortest-path.options.limit' - )} - -
    - { - algorithmAnalyzerStore.mutateSingleSourceWeightedShortestPathParams( - 'limit', - e.value as string - ); - - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'limit' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateSingleSourceWeightedShortestPathParams( - 'limit' - ); - } - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default SingleSourceWeightedShortestPath; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/WeightedShortestPath.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/WeightedShortestPath.tsx deleted file mode 100644 index 26098f650..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/algorithm/WeightedShortestPath.tsx +++ /dev/null @@ -1,500 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Radio, Input, Select, Switch } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; -import { styles } from '../QueryAndAlgorithmLibrary'; -import { Tooltip as CustomTooltip } from '../../../common'; - -import { GraphManagementStoreContext } from '../../../../stores'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import { isDataTypeNumeric, calcAlgorithmFormWidth } from '../../../../utils'; - -import QuestionMarkIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const WeightedShortestPath = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const algorithmAnalyzerStore = dataAnalyzeStore.algorithmAnalyzerStore; - const { t } = useTranslation(); - - const formWidth = calcAlgorithmFormWidth( - graphManagementStore.isExpanded, - 320, - 390 - ); - - const isValidExec = - Object.values( - algorithmAnalyzerStore.validateWeightedShortestPathParamsErrorMessage - ).every((value) => value === '') && - algorithmAnalyzerStore.weightedShortestPathParams.source !== '' && - algorithmAnalyzerStore.weightedShortestPathParams.target !== '' && - algorithmAnalyzerStore.weightedShortestPathParams.weight !== ''; - - return ( -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.source' - )} - -
    - { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - 'source', - e.value as string - ); - - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'source' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'source' - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.label' - )} - -
    - -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.target' - )} - -
    - { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - 'target', - e.value as string - ); - - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'target' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'target' - ); - } - }} - /> -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.max_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - 'max_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'max_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'max_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.direction' - )} - -
    - ) => { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - 'direction', - e.target.value - ); - }} - > - both - out - in - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.skip_degree' - )} - - -
    - { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - 'skip_degree', - e.value as string - ); - - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'skip_degree' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'skip_degree' - ); - } - }} - /> -
    -
    -
    -
    -
    - * - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.weight' - )} - -
    - -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.capacity' - )} - -
    - { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - 'capacity', - e.value as string - ); - - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'capacity' - ); - }} - originInputProps={{ - onBlur() { - algorithmAnalyzerStore.validateWeightedShortestPathParams( - 'capacity' - ); - } - }} - /> -
    -
    -
    -
    -
    - - {t( - 'data-analyze.algorithm-forms.weighted-shortest-path.options.with_vertex' - )} - -
    - { - algorithmAnalyzerStore.mutateWeightedShortestPathParams( - 'with_vertex', - checked - ); - }} - /> -
    -
    -
    - - -
    -
    - ); -}); - -export default WeightedShortestPath; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/common/Favorite.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/common/Favorite.tsx deleted file mode 100644 index 11ea43224..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/common/Favorite.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useContext, useCallback } from 'react'; -import { observer } from 'mobx-react'; -import { Button, Input, Message } from 'hubble-ui'; - -import { DataAnalyzeStoreContext } from '../../../../stores'; -import { useTranslation } from 'react-i18next'; - -export interface FavoriteProps { - handlePop: (flag: boolean) => void; - queryStatement?: string; - isEdit?: boolean; - id?: number; - name?: string; -} - -const styles = { - primaryButton: { - width: 72, - marginRight: 12 - }, - alert: { - width: 320, - fontSize: 12, - marginTop: 4, - color: '#f5535b' - } -}; - -const Favorite: React.FC = observer( - ({ handlePop, queryStatement = '', isEdit = false, id, name = '' }) => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { t } = useTranslation(); - const initialText = isEdit ? name : ''; - const [inputValue, setInputValue] = useState(initialText); - - const handleChange = useCallback(({ value }) => { - setInputValue(value); - }, []); - - const handleAddQueryCollection = useCallback(async () => { - await dataAnalyzeStore.addQueryCollection(inputValue, queryStatement); - - if (dataAnalyzeStore.requestStatus.addQueryCollection === 'success') { - dataAnalyzeStore.setFavoritePopUp(''); - handlePop(false); - setInputValue(''); - - Message.success({ - content: isEdit - ? t('addition.operate.modify-success') - : t('addition.operate.favorite-success'), - size: 'medium', - showCloseIcon: false - }); - - dataAnalyzeStore.fetchFavoriteQueries(); - } - }, [dataAnalyzeStore, handlePop, inputValue, isEdit, queryStatement]); - - const handleEditQueryCollection = useCallback(async () => { - await dataAnalyzeStore.editQueryCollection( - id as number, - inputValue, - queryStatement - ); - - if (dataAnalyzeStore.requestStatus.editQueryCollection === 'success') { - dataAnalyzeStore.setFavoritePopUp(''); - handlePop(false); - setInputValue(''); - - Message.success({ - content: t('addition.operate.modify-success'), - size: 'medium', - showCloseIcon: false - }); - - dataAnalyzeStore.fetchFavoriteQueries(); - } - }, [dataAnalyzeStore, handlePop, id, inputValue, queryStatement]); - - const handleCancel = useCallback( - (type: 'add' | 'edit') => () => { - handlePop(false); - dataAnalyzeStore.setFavoritePopUp(''); - setInputValue(initialText); - dataAnalyzeStore.resetFavoriteRequestStatus(type); - }, - [dataAnalyzeStore, handlePop, initialText] - ); - - return ( -
    -
    - - {isEdit - ? t('addition.operate.modify-name') - : t('addition.operate.favorite-statement')} - - - {dataAnalyzeStore.requestStatus.addQueryCollection === 'failed' && - isEdit === false && ( -
    - {dataAnalyzeStore.errorInfo.addQueryCollection.message} -
    - )} - {dataAnalyzeStore.requestStatus.editQueryCollection === 'failed' && - isEdit === true && ( -
    - {dataAnalyzeStore.errorInfo.editQueryCollection.message} -
    - )} -
    - - -
    -
    -
    - ); - } -); - -export default Favorite; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/index.ts deleted file mode 100644 index dffd5e2e7..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import DataAnalyze from './DataAnalyze'; - -export { DataAnalyze }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/GraphPopOver.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/GraphPopOver.tsx deleted file mode 100644 index 26be682e4..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/GraphPopOver.tsx +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect, useRef, useCallback } from 'react'; -import { observer } from 'mobx-react'; -import { Message } from 'hubble-ui'; -import { isUndefined, size, isEmpty } from 'lodash-es'; - -import { DataAnalyzeStoreContext } from '../../../../stores'; -import { addGraphNodes, addGraphEdges } from '../../../../stores/utils'; -import { useTranslation } from 'react-i18next'; - -interface GraphPopOverProps { - x: number; - y: number; - switchIsPopover: (state: boolean) => void; - isAfterDragging: boolean; - switchAfterDragging: (state: boolean) => void; -} - -const GraphPopOver: React.FC = observer( - ({ x, y, isAfterDragging, switchAfterDragging, switchIsPopover }) => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const popoverWrapperRef = useRef(null); - const { t } = useTranslation(); - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - // if clicked element is not on dropdown, collpase it - if ( - popoverWrapperRef.current && - !popoverWrapperRef.current.contains(e.target as Element) - ) { - if (isAfterDragging) { - switchAfterDragging(false); - return; - } - - switchIsPopover(false); - } - }, - [switchIsPopover, isAfterDragging] - ); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( -
    e.preventDefault()} - style={{ top: y, left: x }} - ref={popoverWrapperRef} - > - {dataAnalyzeStore.rightClickedGraphData.id === '' ? ( -
    { - switchIsPopover(false); - dataAnalyzeStore.setDynamicAddGraphDataStatus('vertex'); - }} - > - {t('addition.common.add-vertex')} -
    - ) : ( - <> -
    { - const node = dataAnalyzeStore.graphData.data.graph_view.vertices.find( - ({ id }) => id === dataAnalyzeStore.rightClickedGraphData.id - ); - - if (isUndefined(node)) { - return; - } - - if (node.label === '~undefined') { - Message.info({ - content: t('addition.message.illegal-vertex'), - size: 'medium', - showCloseIcon: false, - duration: 1 - }); - } - - if ( - isUndefined( - dataAnalyzeStore.vertexTypes.find( - ({ name }) => name === node.label - ) - ) - ) { - return; - } - - await dataAnalyzeStore.expandGraphNode(); - - if ( - dataAnalyzeStore.requestStatus.expandGraphNode === 'success' - ) { - // prompt if there's no extra node - if ( - size( - dataAnalyzeStore.expandedGraphData.data.graph_view - .vertices - ) === 0 - ) { - if ( - isEmpty( - dataAnalyzeStore.visNetwork?.getConnectedNodes(node.id) - ) - ) { - Message.info({ - content: t('addition.message.no-adjacency-points'), - size: 'medium', - showCloseIcon: false, - duration: 1 - }); - } else { - Message.info({ - content: t('addition.message.no-more-points'), - size: 'medium', - showCloseIcon: false, - duration: 1 - }); - } - - return; - } - - addGraphNodes( - dataAnalyzeStore.expandedGraphData.data.graph_view.vertices, - dataAnalyzeStore.visDataSet?.nodes, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexWritingMappings - ); - - addGraphEdges( - dataAnalyzeStore.expandedGraphData.data.graph_view.edges, - dataAnalyzeStore.visDataSet?.edges, - dataAnalyzeStore.edgeColorMappings, - dataAnalyzeStore.edgeThicknessMappings, - dataAnalyzeStore.edgeWithArrowMappings, - dataAnalyzeStore.edgeWritingMappings - ); - - dataAnalyzeStore.resetRightClickedGraphData(); - switchIsPopover(false); - } else { - Message.error({ - content: dataAnalyzeStore.errorInfo.expandGraphNode.message, - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('addition.operate.expand')} -
    -
    { - dataAnalyzeStore.switchShowFilterBoard(true); - switchIsPopover(false); - }} - > - {t('addition.operate.query')} -
    -
    { - dataAnalyzeStore.visDataSet?.nodes.remove([ - dataAnalyzeStore.rightClickedGraphData.id - ]); - dataAnalyzeStore.hideGraphNode( - dataAnalyzeStore.rightClickedGraphData.id - ); - dataAnalyzeStore.resetRightClickedGraphData(); - switchIsPopover(false); - }} - > - {t('addition.operate.hidden')} -
    -
    { - dataAnalyzeStore.setDynamicAddGraphDataStatus('outEdge'); - dataAnalyzeStore.fetchRelatedEdges(); - switchIsPopover(false); - }} - > - {t('addition.common.add-out-edge')} -
    -
    { - dataAnalyzeStore.setDynamicAddGraphDataStatus('inEdge'); - dataAnalyzeStore.fetchRelatedEdges(); - switchIsPopover(false); - }} - > - {t('addition.common.add-in-edge')} -
    - - )} -
    - ); - } -); - -export default GraphPopOver; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/GraphQueryResult.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/GraphQueryResult.tsx deleted file mode 100644 index f6f5d710b..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/GraphQueryResult.tsx +++ /dev/null @@ -1,659 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useState, - useContext, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import { size, isUndefined, isEmpty } from 'lodash-es'; -import { saveAs } from 'file-saver'; -import vis from 'vis-network'; -import 'vis-network/styles/vis-network.min.css'; -import { Message } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import QueryFilterOptions from './QueryFilterOptions'; -import GraphPopOver from './GraphPopOver'; -import { DataAnalyzeStoreContext } from '../../../../stores'; -import { addGraphNodes, addGraphEdges } from '../../../../stores/utils'; - -import ZoomInIcon from '../../../../assets/imgs/ic_fangda_16.svg'; -import ZoomOutIcon from '../../../../assets/imgs/ic_suoxiao_16.svg'; -import CenterIcon from '../../../../assets/imgs/ic_middle_16.svg'; -import DownloadIcon from '../../../../assets/imgs/ic_xiazai_16.svg'; -import FullScreenIcon from '../../../../assets/imgs/ic_quanping_16.svg'; -import ResetScreenIcon from '../../../../assets/imgs/ic_tuichuquanping_16.svg'; -import LoadingBackIcon from '../../../../assets/imgs/ic_loading_back.svg'; -import LoadingFrontIcon from '../../../../assets/imgs/ic_loading_front.svg'; -import AddNodeIcon from '../../../../assets/imgs/ic_add_node.svg'; - -export interface GraphQueryResult { - hidden: boolean; -} - -const GraphQueryResult: React.FC = observer(({ hidden }) => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { t } = useTranslation(); - const graphWrapper = useRef(null); - const resultWrapper = useRef(null); - const legendViewPointWrapper = useRef(null); - const legendWrapper = useRef(null); - const [showLoadingGraphs, switchShowLoadingGraphs] = useState(true); - const [isPopover, switchIsPopover] = useState(false); - const [isAfterDragging, switchAfterDragging] = useState(false); - const [nodeTooltipX, setNodeToolTipX] = useState(0); - const [nodeTooltipY, setNodeToolTipY] = useState(0); - const [legendStep, setLegendStep] = useState(0); - const [legendWidth, setlegendWitdh] = useState(0); - - const [graph, setGraph] = useState(null); - - const redrawGraphs = useCallback(() => { - if (graph) { - const width = getComputedStyle(resultWrapper.current!).width as string; - const height = getComputedStyle(resultWrapper.current!).height as string; - - graph.setSize(width, height); - graph.redraw(); - } - }, [graph]); - - useEffect(() => { - const width = getComputedStyle(resultWrapper.current!).width as string; - const height = getComputedStyle(resultWrapper.current!).height as string; - - const graphNodes = new vis.DataSet(dataAnalyzeStore.graphNodes); - const graphEdges = new vis.DataSet(dataAnalyzeStore.graphEdges); - - if (!graph) { - const data = { - nodes: graphNodes, - edges: graphEdges - }; - - const layout: vis.Options = { - width, - height, - nodes: { - shape: 'dot' - }, - edges: { - arrowStrikethrough: false, - color: { - color: 'rgba(92, 115, 230, 0.8)', - hover: 'rgba(92, 115, 230, 1)', - highlight: 'rgba(92, 115, 230, 1)' - }, - scaling: { - min: 1, - max: 3, - label: { - enabled: false - } - } - }, - interaction: { - hover: true - }, - physics: { - maxVelocity: 50, - solver: 'forceAtlas2Based', - timestep: 0.3, - stabilization: { iterations: 150 } - } - }; - - // initialize your network! - if (graphWrapper.current !== null) { - const network = new vis.Network(graphWrapper!.current, data, layout); - - let timer: number | undefined = undefined; - - network.on('click', ({ nodes, edges }) => { - // click on node, note that edges(related) also has value - if (!isEmpty(nodes)) { - // note: cannot abstract switchClickOn...() and clearTimeout - // as common callings with node and edge, since click event - // would be dispatched even if click is not on node and edge - dataAnalyzeStore.switchClickOnNodeOrEdge(true); - clearTimeout(timer); - - timer = window.setTimeout(() => { - const nodeId = nodes[0]; - const node = - dataAnalyzeStore.graphData.data.graph_view.vertices.find( - ({ id }) => id === nodeId - ); - - if (isUndefined(node)) { - return; - } - - dataAnalyzeStore.changeSelectedGraphData({ - id: node.id, - label: node.label, - properties: node.properties - }); - - dataAnalyzeStore.syncGraphEditableProperties('vertex'); - dataAnalyzeStore.initValidateEditGraphDataPropertiesErrorMessage(); - - if ( - dataAnalyzeStore.graphInfoDataSet !== 'node' || - !dataAnalyzeStore.isShowGraphInfo - ) { - dataAnalyzeStore.switchShowScreeDataSet('node'); - dataAnalyzeStore.switchShowScreenInfo(true); - } - - // close filter board after click on node - dataAnalyzeStore.switchShowFilterBoard(false); - // reset status, or click blank area won't collpase the drawer - dataAnalyzeStore.switchClickOnNodeOrEdge(false); - }, 200); - - return; - } - - // click on edge - if (!isEmpty(edges)) { - dataAnalyzeStore.switchClickOnNodeOrEdge(true); - clearTimeout(timer); - - timer = window.setTimeout(() => { - const edgeId = edges[0]; - - const edge = - dataAnalyzeStore.graphData.data.graph_view.edges.find( - ({ id }) => id === edgeId - ); - - if (isUndefined(edge)) { - return; - } - - dataAnalyzeStore.changeSelectedGraphLinkData({ - id: edge.id, - label: edge.label, - properties: edge.properties, - source: edge.source, - target: edge.target - }); - - dataAnalyzeStore.syncGraphEditableProperties('edge'); - dataAnalyzeStore.initValidateEditGraphDataPropertiesErrorMessage(); - - if ( - dataAnalyzeStore.graphInfoDataSet !== 'edge' || - !dataAnalyzeStore.isShowGraphInfo - ) { - dataAnalyzeStore.switchShowScreeDataSet('edge'); - dataAnalyzeStore.switchShowScreenInfo(true); - } - - // close filter board after click on edge - dataAnalyzeStore.switchShowFilterBoard(false); - // reset status, or click blank area won't collpase the drawer - dataAnalyzeStore.switchClickOnNodeOrEdge(false); - }, 200); - } - }); - - network.on('doubleClick', async ({ nodes }) => { - clearTimeout(timer); - dataAnalyzeStore.switchClickOnNodeOrEdge(false); - if (!isEmpty(nodes)) { - const nodeId = nodes[0]; - const node = - dataAnalyzeStore.graphData.data.graph_view.vertices.find( - ({ id }) => id === nodeId - ); - - if (!isUndefined(node)) { - // specific symbol (~undefined) in schema - if (node.label === '~undefined') { - Message.info({ - content: t('addition.common.illegal-vertex-desc'), - size: 'medium', - showCloseIcon: false, - duration: 1 - }); - } - - if ( - isUndefined( - dataAnalyzeStore.vertexTypes.find( - ({ name }) => name === node.label - ) - ) - ) { - return; - } - - await dataAnalyzeStore.expandGraphNode(node.id, node.label); - - if ( - dataAnalyzeStore.requestStatus.expandGraphNode === 'success' - ) { - // prompt if there's no extra node - if ( - size( - dataAnalyzeStore.expandedGraphData.data.graph_view.vertices - ) === 0 - ) { - if (isEmpty(network.getConnectedNodes(nodeId))) { - Message.info({ - content: t('addition.message.no-adjacency-points'), - size: 'medium', - showCloseIcon: false, - duration: 1 - }); - } else { - Message.info({ - content: t('addition.message.no-more-points'), - size: 'medium', - showCloseIcon: false, - duration: 1 - }); - } - - return; - } - - addGraphNodes( - dataAnalyzeStore.expandedGraphData.data.graph_view.vertices, - dataAnalyzeStore.visDataSet?.nodes, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexWritingMappings - ); - - addGraphEdges( - dataAnalyzeStore.expandedGraphData.data.graph_view.edges, - dataAnalyzeStore.visDataSet?.edges, - dataAnalyzeStore.edgeColorMappings, - dataAnalyzeStore.edgeThicknessMappings, - dataAnalyzeStore.edgeWithArrowMappings, - dataAnalyzeStore.edgeWritingMappings - ); - } - - if (dataAnalyzeStore.requestStatus.expandGraphNode === 'failed') { - Message.error({ - content: dataAnalyzeStore.errorInfo.expandGraphNode.message, - size: 'medium', - showCloseIcon: false - }); - } - } else { - Message.error({ - content: dataAnalyzeStore.errorInfo.expandGraphNode.message, - size: 'medium', - showCloseIcon: false - }); - } - } - }); - - network.on('oncontext', async (e) => { - // disable default context menu - e.event.preventDefault(); - - // It's weird that sometimes e.nodes is empty when right click on node - // thus using coordinate to work as expect - const nodeId = network.getNodeAt(e.pointer.DOM); - const node = dataAnalyzeStore.graphData.data.graph_view.vertices.find( - ({ id }) => id === nodeId - ); - - if (!isUndefined(node)) { - dataAnalyzeStore.changeRightClickedGraphData({ - id: node.id, - label: node.label, - properties: node.properties - }); - - switchIsPopover(true); - - network.selectNodes([nodeId]); - setNodeToolTipX(e.pointer.DOM.x); - setNodeToolTipY(e.pointer.DOM.y); - dataAnalyzeStore.setVisCurrentCoordinates({ - domX: e.pointer.DOM.x, - domY: e.pointer.DOM.y, - canvasX: e.pointer.canvas.x, - canvasY: e.pointer.canvas.y - }); - - await dataAnalyzeStore.fetchRelatedVertex(); - - if (size(dataAnalyzeStore.graphDataEdgeTypes) !== 0) { - dataAnalyzeStore.fetchFilteredPropertyOptions( - dataAnalyzeStore.graphDataEdgeTypes[0] - ); - } - } else { - const edgeId = network.getEdgeAt(e.pointer.DOM); - - // if not click on edge - if (isUndefined(edgeId)) { - dataAnalyzeStore.resetRightClickedGraphData(); - setNodeToolTipX(e.pointer.DOM.x); - setNodeToolTipY(e.pointer.DOM.y); - dataAnalyzeStore.setVisCurrentCoordinates({ - domX: e.pointer.DOM.x, - domY: e.pointer.DOM.y, - canvasX: e.pointer.canvas.x, - canvasY: e.pointer.canvas.y - }); - switchIsPopover(true); - } - } - }); - - network.on('dragging', () => { - const node = dataAnalyzeStore.visDataSet?.nodes.get( - dataAnalyzeStore.rightClickedGraphData.id - ); - - if (node !== null) { - const position = network.getPositions(node.id); - setNodeToolTipX(network.canvasToDOM(position[node.id]).x); - setNodeToolTipY(network.canvasToDOM(position[node.id]).y); - switchAfterDragging(true); - } - }); - - network.on('zoom', () => { - const node = dataAnalyzeStore.visDataSet?.nodes.get( - dataAnalyzeStore.rightClickedGraphData.id - ); - - if (node !== null) { - const position = network.getPositions(node.id); - setNodeToolTipX(network.canvasToDOM(position[node.id]).x); - setNodeToolTipY(network.canvasToDOM(position[node.id]).y); - } - }); - - network.on('dragEnd', (e) => { - if (!isEmpty(e.nodes)) { - network.unselectAll(); - } - }); - - network.once('stabilizationIterationsDone', () => { - switchShowLoadingGraphs(false); - }); - - setGraph(network); - dataAnalyzeStore.setVisNetwork(network); - } - } else { - if (!dataAnalyzeStore.isGraphLoaded) { - graph.setData({ - nodes: graphNodes, - edges: graphEdges - }); - - dataAnalyzeStore.setVisDataSet({ - nodes: graphNodes, - edges: graphEdges - }); - - dataAnalyzeStore.switchGraphLoaded(true); - } - - redrawGraphs(); - } - }, [ - dataAnalyzeStore, - dataAnalyzeStore.originalGraphData, - graph, - dataAnalyzeStore.isFullScreenReuslt, - redrawGraphs - ]); - - useEffect(() => { - if (legendWrapper.current) { - const legendWidth = getComputedStyle(legendWrapper.current).width!.split( - 'px' - )[0]; - - setlegendWitdh(Number(legendWidth)); - } - }, []); - - useEffect(() => { - window.addEventListener('resize', redrawGraphs, false); - - return () => { - window.removeEventListener('resize', redrawGraphs); - }; - }, [redrawGraphs]); - - return ( - <> - - {dataAnalyzeStore.isShowFilterBoard && } - {showLoadingGraphs && ( -
    -
    - {t('addition.operate.load-background')} - {t('addition.operate.load-spinner')} -
    - {t('addition.operate.rendering')}... -
    - )} - - ); -}); - -export default GraphQueryResult; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/JSONQueryResult.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/JSONQueryResult.tsx deleted file mode 100644 index fed89e518..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/JSONQueryResult.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import ReactJsonView from 'react-json-view'; -import 'codemirror/mode/javascript/javascript'; - -import { DataAnalyzeStoreContext } from '../../../../stores'; - -const JSONQueryResult: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - - return ( -
    - -
    - ); -}); - -export default JSONQueryResult; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/QueryFilterOptions.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/QueryFilterOptions.tsx deleted file mode 100644 index 3b8e48afe..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/QueryFilterOptions.tsx +++ /dev/null @@ -1,405 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback } from 'react'; -import { observer } from 'mobx-react'; -import { Select, Input, NumberBox, Calendar } from 'hubble-ui'; -import { Message } from 'hubble-ui'; - -import { DataAnalyzeStoreContext } from '../../../../stores'; -import { addGraphNodes, addGraphEdges } from '../../../../stores/utils'; -import { useTranslation } from 'react-i18next'; -import i18next from '../../../../i18n'; - -const getRuleOptions = (ruleType: string = '') => { - switch (ruleType.toLowerCase()) { - case 'float': - case 'double': - case 'byte': - case 'int': - case 'long': - case 'date': - return [ - i18next.t('addition.constant.greater-than'), - i18next.t('addition.constant.greater-than-or-equal'), - i18next.t('addition.constant.less-than'), - i18next.t('addition.constant.less-than-or-equal'), - i18next.t('addition.constant.equal') - ]; - case 'object': - case 'text': - case 'blob': - case 'uuid': - return [i18next.t('addition.constant.equal')]; - case 'boolean': - return ['True', 'False']; - default: - return []; - } -}; - -const QueryFilterOptions: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { t } = useTranslation(); - const line = dataAnalyzeStore.filteredGraphQueryOptions.line; - const properties = dataAnalyzeStore.filteredGraphQueryOptions.properties; - const lastProperty = properties[properties.length - 1]; - - // value of the corresponding revealed form should not be empty - const allowSendFilterRequest = - (properties.length === 0 && line.type !== '') || - (lastProperty && - lastProperty.property !== '' && - lastProperty.rule !== '' && - lastProperty.value !== '') || - (lastProperty && - (lastProperty.rule === 'True' || lastProperty.rule === 'False')); - - const allowAddProperties = - allowSendFilterRequest && - dataAnalyzeStore.filteredPropertyOptions.length !== 0 && - dataAnalyzeStore.filteredGraphQueryOptions.properties.length !== - dataAnalyzeStore.filteredPropertyOptions.length; - - const handleEdgeSelectChange = useCallback( - (key: 'type' | 'direction') => (value: string) => { - dataAnalyzeStore.editEdgeFilterOption(key, value); - dataAnalyzeStore.fetchFilteredPropertyOptions(value); - }, - [dataAnalyzeStore] - ); - - const handlePropertyChange = useCallback( - ( - key: 'property' | 'rule' | 'value', - value: string | number, - index: number - ) => { - dataAnalyzeStore.editPropertyFilterOption(key, value, index); - }, - [dataAnalyzeStore] - ); - - const renderPropertyValue = ( - type: string = '', - value: string, - index: number - ) => { - const shouldDisabled = - dataAnalyzeStore.filteredGraphQueryOptions.properties[index].property === - ''; - - switch (type.toLowerCase()) { - case 'float': - case 'double': - return ( - { - handlePropertyChange('value', Number(e.value), index); - }} - disabled={shouldDisabled} - /> - ); - case 'byte': - case 'int': - case 'long': - return ( - { - handlePropertyChange('value', Number(e.target.value), index); - }} - disabled={shouldDisabled} - /> - ); - case 'date': - return ( - { - handlePropertyChange('value', timeParams.beginTime, index); - }} - disabled={shouldDisabled} - /> - ); - case 'object': - case 'text': - case 'blob': - case 'uuid': - return ( - { - handlePropertyChange('value', e.value, index); - }} - disabled={shouldDisabled} - /> - ); - case 'boolean': - return
    /
    ; - default: - return ( - - ); - } - }; - - return ( -
    -
    -
    - - {t('addition.common.edge-type')}: - - -
    -
    - - {t('addition.common.edge-direction')}: - - -
    -
    - { - if (!allowSendFilterRequest) { - return; - } - - await dataAnalyzeStore.filterGraphData(); - - if ( - dataAnalyzeStore.requestStatus.filteredGraphData === 'success' - ) { - addGraphNodes( - dataAnalyzeStore.expandedGraphData.data.graph_view.vertices, - dataAnalyzeStore.visDataSet?.nodes, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexWritingMappings - ); - - addGraphEdges( - dataAnalyzeStore.expandedGraphData.data.graph_view.edges, - dataAnalyzeStore.visDataSet?.edges, - dataAnalyzeStore.edgeColorMappings, - dataAnalyzeStore.edgeThicknessMappings, - dataAnalyzeStore.edgeWithArrowMappings, - dataAnalyzeStore.edgeWritingMappings - ); - - // highlight new vertices - if (dataAnalyzeStore.visNetwork !== null) { - dataAnalyzeStore.visNetwork.selectNodes( - dataAnalyzeStore.expandedGraphData.data.graph_view.vertices - .map(({ id }) => id) - .concat([dataAnalyzeStore.rightClickedGraphData.id]), - true - ); - } - - dataAnalyzeStore.switchShowFilterBoard(false); - dataAnalyzeStore.clearFilteredGraphQueryOptions(); - } else { - Message.error({ - content: dataAnalyzeStore.errorInfo.filteredGraphData.message, - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('addition.operate.filter')} - - { - dataAnalyzeStore.switchShowFilterBoard(false); - dataAnalyzeStore.clearFilteredGraphQueryOptions(); - - if (dataAnalyzeStore.visNetwork !== null) { - dataAnalyzeStore.visNetwork.unselectAll(); - } - }} - > - {t('addition.common.cancel')} - -
    -
    - {dataAnalyzeStore.filteredGraphQueryOptions.properties.length !== 0 && ( -
    - )} - {dataAnalyzeStore.filteredGraphQueryOptions.properties.map( - ({ property, rule, value }, index) => { - return ( -
    -
    - {t('addition.common.property')}: - -
    -
    - {t('addition.common.rule')}: - -
    -
    - {t('addition.common.value')}: - {renderPropertyValue( - // the real type of value - dataAnalyzeStore.valueTypes[property], - value, - index - )} -
    -
    - { - dataAnalyzeStore.deletePropertyFilterOption(index); - }} - > - {t('addition.common.del')} - -
    -
    - ); - } - )} -
    - { - dataAnalyzeStore.addPropertyFilterOption(); - } - : undefined - } - style={{ - color: allowAddProperties ? '#2b65ff' : '#ccc' - }} - > - {t('addition.operate.add-filter-item')} - -
    -
    - ); -}); - -export default QueryFilterOptions; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/QueryResult.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/QueryResult.tsx deleted file mode 100644 index 15fa71c2d..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/QueryResult.tsx +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { isEmpty } from 'lodash-es'; -import { useLocation } from 'wouter'; -import classnames from 'classnames'; - -import GraphQueryResult from './GraphQueryResult'; -import TableQueryResult from './TableQueryResult'; -import JSONQueryResult from './JSONQueryResult'; -import { - DataAnalyzeStoreContext, - AsyncTasksStoreContext -} from '../../../../stores'; -import { Algorithm } from '../../../../stores/factory/dataAnalyzeStore/algorithmStore'; -import EmptyIcon from '../../../../assets/imgs/ic_sousuo_empty.svg'; -import LoadingBackIcon from '../../../../assets/imgs/ic_loading_back.svg'; -import LoadingFrontIcon from '../../../../assets/imgs/ic_loading_front.svg'; -import FinishedIcon from '../../../../assets/imgs/ic_done_144.svg'; -import FailedIcon from '../../../../assets/imgs/ic_fail.svg'; -import i18next from '../../../../i18n'; -import { useTranslation } from 'react-i18next'; - -export interface QueryResultProps { - sidebarIndex: number; - handleSetSidebarIndex: (index: number) => void; -} - -const dataAnalyzeContentSidebarOptions = [ - i18next.t('addition.menu.chart'), - i18next.t('addition.menu.table'), - 'Json' -]; - -const QueryResult: React.FC = observer( - ({ sidebarIndex, handleSetSidebarIndex }) => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - const { algorithmAnalyzerStore } = dataAnalyzeStore; - const asyncTasksStore = useContext(AsyncTasksStoreContext); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - const renderReuslt = (index: number) => { - switch (index) { - case 1: - return ; - case 2: - return ; - } - }; - - const queryResutlClassName = classnames({ - 'query-result': true, - 'query-result-fullscreen': dataAnalyzeStore.isFullScreenReuslt - }); - - const dynHeightStyle: Record = {}; - - if ( - dataAnalyzeStore.currentTab === 'algorithm-analyze' && - !algorithmAnalyzerStore.isCollapse - ) { - if (algorithmAnalyzerStore.currentAlgorithm === '') { - dynHeightStyle.height = 'calc(100vh - 441px)'; - } - } - - return ( -
    - {!dataAnalyzeStore.isFullScreenReuslt && ( -
    - {dataAnalyzeContentSidebarOptions.map((text, index) => ( -
    -
    { - handleSetSidebarIndex(index); - }} - className={ - sidebarIndex === index - ? 'query-result-sidebar-options-active' - : '' - } - > - - {text} -
    -
    - ))} -
    - )} -
    - {dataAnalyzeStore.requestStatus.fetchGraphs === 'success' && - renderReuslt(sidebarIndex)} - - {dataAnalyzeStore.requestStatus.fetchGraphs === 'success' && - dataAnalyzeStore.graphData.data.graph_view.vertices !== null && - dataAnalyzeStore.graphData.data.graph_view.edges !== null && - !isEmpty(dataAnalyzeStore.graphData.data.graph_view.vertices) && ( -
    -
    - ); - } -); - -export default QueryResult; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/TableQueryResult.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/TableQueryResult.tsx deleted file mode 100644 index 1ee0764be..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/TableQueryResult.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback } from 'react'; -import { observer } from 'mobx-react'; -import { Table } from 'hubble-ui'; -import { size } from 'lodash-es'; - -import { DataAnalyzeStoreContext } from '../../../../stores'; - -const TableQueryResult: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStoreContext); - - const columnConfigs = dataAnalyzeStore.originalGraphData.data.table_view.header.map( - (title) => ({ - title, - dataIndex: title, - width: - 100 / size(dataAnalyzeStore.originalGraphData.data.table_view.header) + - '%', - render(text: any) { - if (title === 'path') { - return ; - } - - return JSON.stringify(text); - } - }) - ); - - const handlePageChange = useCallback( - (e: React.ChangeEvent) => { - dataAnalyzeStore.mutatePageNumber('tableResult', Number(e.target.value)); - }, - [dataAnalyzeStore] - ); - - return ( -
    -
    - - ); -}); - -// item could be obejct array which needs serialization as well -const PathItem: React.FC<{ items: string[] }> = observer(({ items }) => ( - <> - {items.map((item: string) => ( - - {JSON.stringify(item)} - - ))} - -)); - -export default TableQueryResult; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/index.ts deleted file mode 100644 index 129ccb2f7..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-analyze/query-result/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import QueryResult from './QueryResult'; - -export { QueryResult }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportFinish.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportFinish.tsx deleted file mode 100644 index 63cb5c8cc..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportFinish.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { useRoute, useLocation } from 'wouter'; -import { isEmpty } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import { Button } from 'hubble-ui'; - -import { - ImportManagerStoreContext, - GraphManagementStoreContext, - DataImportRootStoreContext -} from '../../../../stores'; - -import { useInitDataImport } from '../../../../hooks'; - -import PassIcon from '../../../../assets/imgs/ic_pass.svg'; - -const ImportFinish: React.FC = observer(() => { - const importManagerStore = useContext(ImportManagerStoreContext); - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [, params] = useRoute( - '/graph-management/:id/data-import/import-manager/:jobId/import-tasks/:status*' - ); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - useEffect(() => { - if (isEmpty(serverDataImportStore.importTasks) && params !== null) { - dataImportRootStore.setCurrentId(Number(params.id)); - dataImportRootStore.setCurrentJobId(Number(params.jobId)); - - serverDataImportStore.fetchAllImportTasks(); - } - }, [params?.id, params?.jobId]); - - return ( -
    -
    - complete -
    -
    {t('data-import-status.finished')}
    -
    - {t('data-import-status.success', { - number: - serverDataImportStore.successImportFileStatusNumber !== 0 - ? serverDataImportStore.successImportFileStatusNumber - : '-' - })} - {serverDataImportStore.pausedImportFileNumber !== 0 && - `,${t('data-import-status.pause', { - number: serverDataImportStore.pausedImportFileNumber - })}`} - {serverDataImportStore.abortImportFileNumber !== 0 && - `,${t('data-import-status.abort', { - number: serverDataImportStore.abortImportFileNumber - })}`} -
    -
    -
    -
    - -
    -
    - ); -}); - -export default ImportFinish; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportManager.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportManager.less deleted file mode 100644 index f04a4c2c1..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportManager.less +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.import-manager { - position: absolute; - width: calc(100% - 60px); - padding: 0 16px 16px; - left: 60px; - top: 60px; - - &-with-expand-sidebar { - width: calc(100% - 200px); - left: 200px; - } - - &-content-wrapper { - height: calc(100vh - 130px); - background: #fff; - padding: 16px; - overflow: auto; - } - - &-content-header { - margin-bottom: 16px; - display: flex; - justify-content: flex-end; - } - - &-breadcrumb-wrapper { - display: flex; - justify-content: space-between; - align-items: center; - margin: 16px 0; - } - - &-empty-list { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - & > div { - margin: 8px 0 24px; - } - } - - &-table { - &-job-name { - display: flex; - - & > .link { - color: #2b65ff; - cursor: pointer; - } - - & > img { - display: block; - margin-left: 6px; - cursor: pointer; - } - } - - &-status-wrapper { - height: 28px; - text-align: center; - border-radius: 2px; - line-height: 28px; - font-size: 14px; - } - - &-status-pending { - width: 58px; - border: 1px solid #e0e0e0; - background-color: #f5f5f5; - color: #333; - } - - &-status-process { - width: 58px; - background-color: #f2f7ff; - border: 1px solid #8cb8ff; - color: #3d88f2; - } - - &-status-failed { - width: 44px; - border: 1px solid #ff9499; - background-color: #fff2f2; - color: #e64552; - } - - &-status-success { - width: 44px; - border: 1px solid #7ed988; - background-color: #f2fff4; - color: #39bf45; - } - - &-manipulations { - display: flex; - justify-content: flex-end; - color: #2b65ff; - width: 100px; - - & > span { - cursor: pointer; - - &:last-child { - margin-left: 16px; - } - } - - &-outlink { - font-size: 14px; - color: #2b65ff; - line-height: 22px; - cursor: pointer; - display: flex; - width: fit-content; - text-decoration: none; - - &:hover { - color: #527dff; - } - - &:active { - color: #184bcc; - } - - &-disabled { - color: #999; - } - } - } - } - - &-create-job-option { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 32px; - - &:first-child { - margin-top: 8px; - } - - &:last-child { - margin-bottom: 12px; - } - - & > div:first-child { - width: 78px; - text-align: right; - } - - &-required-mark { - color: #d0021b; - } - } - - &-delete-job-option { - & span { - display: block; - } - } -} - -/* overrides */ - -// reset breadcrumb line-height -.import-manager .new-fc-one-breadcrumb.new-fc-one-breadcrumb-small { - line-height: 22px; -} - -.import-manager .new-fc-one-menu { - background-color: transparent; -} - -.import-manager-content-wrapper { - & table { - table-layout: fixed; - } -} - -.import-manager .new-fc-one-breadcrumb > span:not(:last-of-type) .new-fc-one-breadcrumb-link { - color: #2b65ff; -} - -// weired, when checking img block in devtool -// its size doesn't match what it seems -// need to hard-code it's left px here -// .import-management-table-job-name .new-fc-one-tooltip { -// left: 323px !important; -// } - -// remove horizon padding of close icon in -.new-fc-one-modal-close-x { - padding: 8px 0; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportManager.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportManager.tsx deleted file mode 100644 index 9f34968c9..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportManager.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { useRoute, useLocation, Switch, Route } from 'wouter'; -import { useTranslation } from 'react-i18next'; -import { isNull } from 'lodash-es'; -import classnames from 'classnames'; -import { Breadcrumb } from 'hubble-ui'; - -import { JobDetails } from './job-details'; -import ImportTaskList from './ImportTaskList'; -import { - GraphManagementStoreContext, - DataImportRootStoreContext, - ImportManagerStoreContext -} from '../../../../stores'; - -import './ImportManager.less'; -import ImportTasks from './ImportTasks'; - -const ImportManager: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const dataImportRootStore = useContext(DataImportRootStoreContext); - const importManagerStore = useContext(ImportManagerStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [_, params] = useRoute( - '/graph-management/:id/data-import/import-manager/:rest*' - ); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - const wrapperClassName = classnames({ - 'import-manager': true, - 'import-manager-with-expand-sidebar': graphManagementStore.isExpanded - }); - - useEffect(() => { - window.scrollTo(0, 0); - - graphManagementStore.fetchIdList(); - importManagerStore.setCurrentId(Number(params!.id)); - - return () => { - importManagerStore.dispose(); - }; - }, []); - - return ( -
    -
    - - { - if (!isNull(importManagerStore.selectedJob)) { - setLocation( - `/graph-management/${importManagerStore.currentId}/data-import/import-manager` - ); - importManagerStore.setSelectedJob(null); - importManagerStore.fetchImportJobList(); - } - - // reset stores - // dataMapStore.dispose(); - // serverDataImportStore.dispose(); - }} - > - {t('breadcrumb.first')} - - {importManagerStore.selectedJob && ( - - {importManagerStore.selectedJob.job_name} - - )} - -
    - - - - - -
    - ); -}); - -export default ImportManager; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTaskList.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTaskList.tsx deleted file mode 100644 index 13168cf4e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTaskList.tsx +++ /dev/null @@ -1,768 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { useRoute, useLocation } from 'wouter'; -import { useTranslation } from 'react-i18next'; -import { isEmpty, size } from 'lodash-es'; -import classnames from 'classnames'; -import { Button, Input, Table, Modal, Message } from 'hubble-ui'; - -import LoadingDataView from '../../../common/LoadingDataView'; -import { Tooltip as CustomTooltip } from '../../../common'; -import { - DataImportRootStoreContext, - ImportManagerStoreContext -} from '../../../../stores'; - -import AddIcon from '../../../../assets/imgs/ic_add.svg'; -import HintIcon from '../../../../assets/imgs/ic_question_mark.svg'; - -const styles = { - button: { - width: 78, - marginLeft: 12 - } -}; - -const ImportTaskList: React.FC = observer(() => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const importManagerStore = useContext(ImportManagerStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [preLoading, switchPreLoading] = useState(true); - const [isPopCreateModal, switchCreatePopModal] = useState(false); - const [, params] = useRoute( - '/graph-management/:id/data-import/import-manager' - ); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - const isLoading = - preLoading || - importManagerStore.requestStatus.fetchImportJobList === 'pending'; - - const handleSearchChange = (e: React.ChangeEvent) => { - importManagerStore.mutateSearchWords(e.target.value); - }; - - const handleSearch = async () => { - importManagerStore.mutateImportJobListPageNumber(1); - importManagerStore.switchSearchedStatus(true); - await importManagerStore.fetchImportJobList(); - }; - - const handleClearSearch = () => { - importManagerStore.mutateSearchWords(''); - importManagerStore.mutateImportJobListPageNumber(1); - importManagerStore.switchSearchedStatus(false); - importManagerStore.fetchImportJobList(); - }; - - const handlePageChange = (e: React.ChangeEvent) => { - importManagerStore.mutateImportJobListPageNumber(Number(e.target.value)); - importManagerStore.fetchImportJobList(); - }; - - const columnConfigs = [ - { - title: t('import-manager.list-column-title.job-name'), - dataIndex: 'job_name', - width: '20%', - render(name: string, rowData: any) { - const readyToJump = - rowData.job_status === 'SUCCESS' || rowData.job_status === 'FAILED'; - - const wrapperClassName = classnames({ - 'no-line-break': true, - link: readyToJump - }); - - return ( -
    -
    { - if (readyToJump) { - importManagerStore.setCurrentJobDetailStep('basic'); - importManagerStore.setSelectedJob(rowData.id); - - // fill in essential data in import-task stores - dataImportRootStore.setCurrentId(Number(params!.id)); - dataImportRootStore.setCurrentJobId(rowData.id); - - dataImportRootStore.fetchVertexTypeList(); - dataImportRootStore.fetchEdgeTypeList(); - - setLocation( - `/graph-management/${ - params!.id - }/data-import/import-manager/${rowData.id}/details` - ); - - // fetch related data - await Promise.all([ - dataMapStore.fetchDataMaps(), - serverDataImportStore.fetchAllImportTasks() - ]); - - dataMapStore.setSelectedFileId( - Number(dataMapStore.fileMapInfos[0].id) - ); - dataMapStore.setSelectedFileInfo(); - - // set flags about readonly and irregular process in - dataMapStore.switchReadOnly(true); - dataMapStore.switchIrregularProcess(true); - - // set flags about readonly and irregular process in - serverDataImportStore.switchExpandImportConfig(true); - serverDataImportStore.switchReadOnly(true); - serverDataImportStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - } - }} - > - {name} -
    - {!isEmpty(rowData.job_remarks) && ( - - )} -
    - ); - } - }, - { - title: t('import-manager.list-column-title.size'), - dataIndex: 'job_size', - width: '15%', - render(text: any) { - return
    {text}
    ; - } - }, - { - title: t('import-manager.list-column-title.create-time'), - dataIndex: 'create_time', - render(text: string) { - return
    {text}
    ; - } - }, - { - title: t('import-manager.list-column-title.status'), - dataIndex: 'job_status', - render(text: string) { - let specificClassName = ''; - - switch (text) { - case 'DEFAULT': - case 'UPLOADING': - specificClassName = 'import-manager-table-status-pending'; - break; - case 'MAPPING': - case 'SETTING': - case 'LOADING': - specificClassName = 'import-manager-table-status-process'; - break; - case 'FAILED': - specificClassName = 'import-manager-table-status-failed'; - break; - case 'SUCCESS': - specificClassName = 'import-manager-table-status-success'; - break; - } - - return ( -
    - {t(`import-manager.list-column-status.${text}`)} -
    - ); - } - }, - { - title: t('import-manager.list-column-title.time-consuming'), - dataIndex: 'job_duration', - render(text: string) { - return
    {text}
    ; - } - }, - { - title: t('import-manager.list-column-title.manipulation'), - render(_: any, rowData: any) { - return ( -
    - -
    - ); - } - } - ]; - - useEffect(() => { - if (importManagerStore.currentId !== null) { - setTimeout(() => { - switchPreLoading(false); - }, 800); - - importManagerStore.fetchImportJobList(); - } - }, [importManagerStore.currentId]); - - return ( -
    -
    - - -
    -
    -
    {t('import-manager.hint.no-result')} - ) : ( - - ) - } - /> - ) - }} - dataSource={isLoading ? [] : importManagerStore.importJobList} - pagination={ - isLoading - ? null - : { - hideOnSinglePage: false, - pageNo: importManagerStore.importJobListPageConfig.pageNumber, - pageSize: 10, - showSizeChange: false, - showPageJumper: false, - total: importManagerStore.importJobListPageConfig.pageTotal, - onPageNoChange: handlePageChange - } - } - /> - - { - switchCreatePopModal(false); - await importManagerStore.createNewJob(); - importManagerStore.resetJob('new'); - - if (importManagerStore.requestStatus.createNewJob === 'success') { - Message.success({ - content: t('import-manager.hint.creation-succeed'), - size: 'medium', - showCloseIcon: false - }); - - importManagerStore.fetchImportJobList(); - return; - } - - if (importManagerStore.requestStatus.createNewJob === 'failed') { - Message.error({ - content: importManagerStore.errorInfo.createNewJob.message, - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('import-manager.modal.manipulations.create')} - , - - ]} - destroyOnClose - needCloseIcon={true} - onCancel={() => { - switchCreatePopModal(false); - importManagerStore.resetJob('new'); - }} - > -
    -
    -
    - - * - - {t('import-manager.modal.create-job.job-name')} -
    - { - importManagerStore.mutateNewJob('name', e.value); - importManagerStore.validateJob('new', 'name'); - }} - originInputProps={{ - onBlur: () => { - importManagerStore.validateJob('new', 'name'); - } - }} - /> -
    -
    -
    {t('import-manager.modal.create-job.job-description')}
    -
    - { - importManagerStore.mutateNewJob('description', e.value); - importManagerStore.validateJob('new', 'description'); - }} - originInputProps={{ - onBlur: () => { - importManagerStore.validateJob('new', 'description'); - } - }} - /> -
    -
    -
    -
    - - ); -}); - -export interface ImportManagerManipulationProps { - jobId: number; - jobName: string; - status: string; -} - -export const ImportManagerManipulation: React.FC = observer( - ({ jobId, jobName, status }) => { - const importManagerStore = useContext(ImportManagerStoreContext); - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [isPopDeleteModal, switchPopDeleteModal] = useState(false); - const [, params] = useRoute( - '/graph-management/:id/data-import/import-manager' - ); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - const jumpToLoaction = (step: number, jobName: string) => async () => { - importManagerStore.setSelectedJob(jobId); - - dataImportRootStore.setCurrentId(Number(params!.id)); - dataImportRootStore.setCurrentJobId(jobId); - dataImportRootStore.setCurrentStatus(status); - - let route = ''; - - if (step === 1) { - await dataMapStore.fetchDataMaps(); - route = 'upload'; - } - - if (step === 2) { - // users may browse from - dataMapStore.switchReadOnly(false); - - await dataMapStore.fetchDataMaps(); - dataMapStore.setSelectedFileId(dataMapStore.fileMapInfos[0].id); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - - route = 'mapping'; - } - - if (step === 3) { - // users may browse from - serverDataImportStore.switchReadOnly(false); - - await dataMapStore.fetchDataMaps(); - - // need to set default selected file - dataMapStore.setSelectedFileId(dataMapStore.fileMapInfos[0].id); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - serverDataImportStore.switchIrregularProcess(true); - - if (status === 'SETTING') { - // user may browse from to - dataMapStore.switchReadOnly(false); - - serverDataImportStore.resetImportTasks(); - serverDataImportStore.switchFetchImportStatus('standby'); - serverDataImportStore.switchImportFinished(false); - } - - if (status === 'LOADING') { - // reveal previous & next button in - // users may browse from which set @readonly true - dataMapStore.switchReadOnly(false); - dataMapStore.switchLock(true); - serverDataImportStore.switchImportConfigReadOnly(true); - // users may browse from , let store fetches - // for one time and decide whether import is finished - serverDataImportStore.resetImportTasks(); - serverDataImportStore.switchImportFinished(false); - } - - route = 'loading'; - } - - dataImportRootStore.setCurrentStep(step); - - setLocation( - `/graph-management/${ - params!.id - }/data-import/import-manager/${jobId}/import-tasks/${route}` - ); - }; - - return ( -
    - {(status === 'DEFAULT' || status === 'UPLOADING') && ( - - {t('import-manager.list-column-manipulations.start')} - - )} - {status === 'MAPPING' && ( - - {t('import-manager.list-column-manipulations.resume-setting')} - - )} - {(status === 'SETTING' || status === 'LOADING') && ( - - {t('import-manager.list-column-manipulations.resume-importing')} - - )} - {status === 'FAILED' && ( - // { - // setLocation( - // `/graph-management/${ - // params!.id - // }/data-import/job-error-log/${jobId}` - // ); - // }} - // > - // {t('import-manager.list-column-manipulations.check-error-log')} - // - - {t('import-manager.list-column-manipulations.check-error-log')} - - )} - { - switchPopDeleteModal(true); - }} - > - {t('import-manager.list-column-manipulations.delete')} - - { - switchPopDeleteModal(false); - await importManagerStore.deleteJob(jobId); - importManagerStore.fetchImportJobList(); - }} - key="delete" - > - {t('import-manager.modal.manipulations.delete')} - , - - ]} - destroyOnClose - needCloseIcon={true} - onCancel={() => { - switchPopDeleteModal(false); - }} - > -
    - - {t('import-manager.modal.delete-job.hint', { - name: jobName - })} - - {t('import-manager.modal.delete-job.sub-hint')} -
    -
    -
    - ); - } -); - -export const EmptyImportHints: React.FC = observer(() => { - const importManagerStore = useContext(ImportManagerStoreContext); - const [isPopCreateModal, switchCreatePopModal] = useState(false); - const { t } = useTranslation(); - - return ( -
    - {t('import-manager.manipulation.create')} -
    {t('import-manager.hint.empty-task')}
    - - { - switchCreatePopModal(false); - await importManagerStore.createNewJob(); - importManagerStore.resetJob('new'); - - if (importManagerStore.requestStatus.createNewJob === 'success') { - Message.success({ - content: t('import-manager.hint.creation-succeed'), - size: 'medium', - showCloseIcon: false - }); - - importManagerStore.fetchImportJobList(); - return; - } - - if (importManagerStore.requestStatus.createNewJob === 'failed') { - Message.error({ - content: importManagerStore.errorInfo.createNewJob.message, - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('import-manager.modal.manipulations.create')} - , - - ]} - destroyOnClose - needCloseIcon={true} - onCancel={() => { - switchCreatePopModal(false); - importManagerStore.resetJob('new'); - }} - > -
    -
    -
    - - * - - {t('import-manager.modal.create-job.job-name')} -
    - { - importManagerStore.mutateNewJob('name', e.value); - importManagerStore.validateJob('new', 'name'); - }} - onBlur={() => { - importManagerStore.validateJob('new', 'name'); - }} - /> -
    -
    -
    {t('import-manager.modal.create-job.job-description')}
    -
    - { - importManagerStore.mutateNewJob('description', e.value); - importManagerStore.validateJob('new', 'description'); - }} - onBlur={() => { - importManagerStore.validateJob('new', 'description'); - }} - /> -
    -
    -
    -
    -
    - ); -}); - -export default ImportTaskList; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTasks.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTasks.less deleted file mode 100644 index a93c3eddc..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTasks.less +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.import-tasks { - &-breadcrumb-wrapper { - margin: 16px 0; - } - - &-content-wrapper { - height: calc(100vh - 130px); - background: #fff; - } - - &-step-wrapper { - display: flex; - height: calc(100vh - 194px); - overflow: auto; - } - - &-step-content-header { - display: flex; - margin-bottom: 16px; - - & > span { - font-weight: 900; - line-height: 24px; - } - - & img { - cursor: pointer; - } - - &-expand { - margin-left: 9.7px; - transform: rotate(-180deg); - transition: transform 0.3s; - } - - &-collpase { - margin-left: 9.7px; - transform: rotate(0deg); - transition: transform 0.3s; - } - } - - &-tooltips { - padding: 16px; - color: #ff5b5b; - } - - &-manipulation { - font-size: 14px; - color: #2b65ff; - line-height: 20px; - cursor: pointer; - display: flex; - width: fit-content; - - &-disabled { - color: #999; - } - } - - &-complete-hint { - display: flex; - height: calc(100vh - 194px); - flex-direction: column; - justify-content: center; - align-items: center; - - &-description { - display: flex; - justify-content: center; - - & > div { - margin-left: 20px; - - & > div:first-of-type { - font-size: 24px; - } - - & > div:last-of-type { - color: #333; - } - } - } - - &-manipulations { - margin: 42px auto 0; - } - } -} - -// overrides -.import-tasks .new-fc-one-breadcrumb.new-fc-one-breadcrumb-small { - line-height: 22px; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTasks.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTasks.tsx deleted file mode 100644 index 3d951614e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/ImportTasks.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useMemo, useEffect, useLayoutEffect } from 'react'; -import { observer } from 'mobx-react'; -import { useRoute, useLocation } from 'wouter'; -import { isNull } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Steps, Button } from 'hubble-ui'; - -import UploadEntry from './UploadEntry'; -import { DataMapConfigs } from './datamap-configs'; -import { ServerDataImport } from './server-data-import'; -import ImportFinish from './ImportFinish'; -import { - ImportManagerStoreContext, - GraphManagementStoreContext, - DataImportRootStoreContext -} from '../../../../stores'; - -import PassIcon from '../../../../assets/imgs/ic_pass.svg'; - -import './ImportTasks.less'; - -const ImportTasks: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const importManagerStore = useContext(ImportManagerStoreContext); - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [, params] = useRoute( - '/graph-management/:id/data-import/import-manager/:jobId/import-tasks/:status*' - ); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - const steps = useMemo( - () => [ - t('step.first'), - t('step.second'), - t('step.third'), - t('step.fourth') - ], - [] - ); - - const wrapperClassName = classnames({ - 'import-tasks': true, - 'import-tasks-with-expand-sidebar': graphManagementStore.isExpanded - }); - - useEffect(() => { - if (!isNull(params)) { - switch (params.status) { - case 'upload': - dataImportRootStore.setCurrentStep(1); - break; - case 'mapping': - dataImportRootStore.setCurrentStep(2); - break; - case 'loading': - dataImportRootStore.setCurrentStep(3); - break; - case 'finish': - dataImportRootStore.setCurrentStep(4); - break; - } - } - }, [params?.status]); - - useEffect(() => { - window.scrollTo(0, 0); - dataImportRootStore.setCurrentJobId(Number(params!.jobId)); - - graphManagementStore.fetchIdList(); - dataImportRootStore.setCurrentId(Number(params!.id)); - dataImportRootStore.fetchVertexTypeList(); - dataImportRootStore.fetchEdgeTypeList(); - dataMapStore.fetchDataMaps(); - - return () => { - // no specific job here, solve the problem that click back button in browser - // since relies on @selectedJob in useEffect() - importManagerStore.setSelectedJob(null); - dataImportRootStore.dispose(); - dataMapStore.dispose(); - serverDataImportStore.dispose(); - }; - }, []); - - return ( -
    -
    -
    - - {steps.map((title: string, index: number) => ( - index + 1 - ? 'finish' - : 'wait' - } - key={title} - /> - ))} - -
    - {dataImportRootStore.currentStep === 1 && } - {dataImportRootStore.currentStep === 2 && } - {dataImportRootStore.currentStep === 3 && } - {dataImportRootStore.currentStep === 4 && } -
    -
    - ); -}); - -export default ImportTasks; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/UploadEntry.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/UploadEntry.less deleted file mode 100644 index 7af02e583..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/UploadEntry.less +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.import-tasks { - &-upload-wrapper { - padding: 0 16px 28px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: calc(100vh - 250px); - } - - &-upload-drag-area { - width: 400px; - height: 200px; - padding: 83px 73px; - border: 1px dashed #979797; - // display: flex; - // justify-content: center; - // align-items: center; - font-size: 12px; - color: #666; - - &:hover, - &.file-above { - border-color: #2b65ff; - } - } - - &-upload-file-list { - margin-top: 30px; - width: 400px; - max-height: calc(100vh - 512px); - overflow-y: auto; - overflow-x: hidden; - font-size: 14px; - line-height: 20px; - color: #333; - } - - &-upload-file-info { - margin-bottom: 19px; - - &-titles { - width: 75%; - display: flex; - justify-content: space-between; - } - - &-progress-status { - width: 400px; - display: flex; - align-items: center; - - &-refresh-icon { - position: relative; - left: -22px; - cursor: pointer; - } - - &-close-icon { - position: relative; - left: -35px; - cursor: pointer; - top: 0; - - &.in-progress { - left: 0; - top: 1px; - } - - &.in-error { - left: -12px; - top: 0; - } - } - } - } - - &-manipulation-wrapper { - display: flex; - justify-content: center; - } -} - -/* override */ - -// disable original delete icon in progress bar due to the img bug -.import-tasks-upload-file-info .new-fc-one-progress-operation { - display: none; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/UploadEntry.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/UploadEntry.tsx deleted file mode 100644 index 0449f24c9..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/UploadEntry.tsx +++ /dev/null @@ -1,1037 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback, useRef, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { useLocation } from 'wouter'; -import classnames from 'classnames'; -import { - isEmpty, - size, - isUndefined, - range, - xor, - intersection -} from 'lodash-es'; -import { DndProvider, useDrop, DropTargetMonitor } from 'react-dnd'; -import { useTranslation } from 'react-i18next'; -import { HTML5Backend, NativeTypes } from 'react-dnd-html5-backend'; -import { Button, Progress, Message } from 'hubble-ui'; -import { CancellablePromise } from 'mobx/lib/api/flow'; - -import { DataImportRootStoreContext } from '../../../../stores'; -import { useInitDataImport } from '../../../../hooks'; -import { isCurrentJobUploadSizeExceeded } from '../../../../utils/dataImportUpload'; - -import type { FileUploadResult } from '../../../../stores/types/GraphManagementStore/dataImportStore'; - -import CloseIcon from '../../../../assets/imgs/ic_close_16.svg'; -import RefreshIcon from '../../../../assets/imgs/ic_refresh.svg'; - -import './UploadEntry.less'; - -const KB = 1024; -const MB = 1024 * 1024; -const GB = 1024 * 1024 * 1024; -const MAX_CONCURRENT_UPLOAD = 5; - -const UploadEntry: React.FC = observer(() => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const isInitReady = useInitDataImport(); - const { t } = useTranslation(); - const [, setLocation] = useLocation(); - - useEffect(() => { - const unload = (e: any) => { - e = e || window.event; - - if (e) { - e.returnValue = 'hint'; - } - - return 'hint'; - }; - - window.addEventListener('beforeunload', unload); - - return () => { - window.removeEventListener('beforeunload', unload); - }; - }, [dataImportRootStore]); - - return isInitReady ? ( - <> - - - -
    - - -
    - - ) : null; -}); - -export const FileDropZone: React.FC = observer(() => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [{ canDrop, isOver }, drop] = useDrop({ - accept: [NativeTypes.FILE], - drop(item, monitor) { - handleFileDrop(monitor); - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop() - }) - }); - const { t } = useTranslation(); - - const uploadRef = useRef(null); - - const validateFileCondition = (files: File[]) => { - const validatedFiles = []; - const currentUploadFileNames = dataImportRootStore.fileUploadTasks - .map(({ name }) => name) - .concat(dataMapStore.fileMapInfos.map(({ name }) => name)); - - const filteredFiles = files.filter( - ({ name }) => !currentUploadFileNames.includes(name) - ); - - if (size(filteredFiles) !== size(files)) { - const duplicatedFiles = xor(files, filteredFiles); - - Message.error({ - content: ( -
    -

    {t('upload-files.no-duplicate')}

    - {duplicatedFiles.map((file) => ( -

    {file.name}

    - ))} -
    - ), - size: 'medium', - showCloseIcon: false - }); - } - - if ( - isCurrentJobUploadSizeExceeded( - dataMapStore.fileMapInfos, - dataImportRootStore.fileUploadTasks, - filteredFiles - ) - ) { - Message.error({ - content: `${t('upload-files.over-all-size-limit')}`, - size: 'medium', - showCloseIcon: false - }); - - return []; - } - - for (const file of filteredFiles) { - const { name, size } = file; - const sizeGB = size / GB; - - if (size === 0) { - Message.error({ - content: `${name} ${t('upload-files.empty-file')}`, - size: 'medium', - showCloseIcon: false - }); - - break; - } - - if (name.slice(-4) !== '.csv') { - Message.error({ - content: `${name}: ${t('upload-files.wrong-format')}`, - size: 'medium', - showCloseIcon: false - }); - - break; - } - - if (sizeGB > 1) { - Message.error({ - content: `${name}: ${t('upload-files.over-single-size-limit')}`, - size: 'medium', - showCloseIcon: false - }); - - break; - } - - validatedFiles.push(file); - } - - dataImportRootStore.updateFileList(validatedFiles); - return validatedFiles; - }; - - const handleFileChange = (files: File[]) => { - for (const file of files) { - const { name, size } = file; - const sizeMB = size / MB; - - const chunkList = []; - // start byte to slice - let currentChunkStartIndex = 0; - // size of each chunk - let chunkSizeMB: number; - let chunkIndex = 0; - - if (sizeMB > 2 && sizeMB <= 128) { - chunkSizeMB = 2; - } else if (sizeMB > 128 && size <= 512) { - chunkSizeMB = 4; - } else { - chunkSizeMB = 5; - } - - while (currentChunkStartIndex < size) { - chunkList.push({ - chunkIndex, - chunk: file.slice( - currentChunkStartIndex, - currentChunkStartIndex + chunkSizeMB * MB - ) - }); - currentChunkStartIndex += chunkSizeMB * MB; - ++chunkIndex; - } - - dataImportRootStore.initFileUploadTask({ - name, - size, - status: 'uploading', - chunkList, - chunkTotal: Math.ceil(size / (chunkSizeMB * MB)), - uploadedChunkTotal: 0, - pendingChunkIndexes: [], - failedChunkIndexes: [], - uploadedChunksIndexes: [] - }); - } - }; - - const initFileTaskQueue = () => { - const firstRequestTasks = dataImportRootStore.fileUploadTasks.slice( - 0, - MAX_CONCURRENT_UPLOAD - ); - - firstRequestTasks.forEach(({ name, chunkList, chunkTotal }) => { - const task = dataImportRootStore.uploadFiles({ - fileName: name, - fileChunkList: chunkList[0], - fileChunkTotal: chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName: name, - status: 'uploading', - task - }); - - scheduler(name, 0, task); - }); - - if (size(firstRequestTasks) < MAX_CONCURRENT_UPLOAD) { - let loopCount = 1; - - // Traverse firstRequestTasks to add extra items to queue - while (loopCount) { - const currentQueueItemSize = size(dataImportRootStore.fileUploadQueue); - - firstRequestTasks.forEach(({ chunkTotal, chunkList, name }) => { - if ( - chunkTotal > loopCount && - size(dataImportRootStore.fileUploadQueue) < MAX_CONCURRENT_UPLOAD - ) { - const task = dataImportRootStore.uploadFiles({ - fileName: name, - fileChunkList: chunkList[loopCount], - fileChunkTotal: chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName: name, - status: 'uploading', - task - }); - - scheduler(name, loopCount, task); - } - }); - - if ( - size(dataImportRootStore.fileUploadQueue) === MAX_CONCURRENT_UPLOAD || - // which means no other items pushed into queue - currentQueueItemSize === size(dataImportRootStore.fileUploadQueue) - ) { - break; - } else { - loopCount += 1; - } - } - } - }; - - const scheduler = useCallback( - async ( - fileName: string, - fileChunkIndex: number, - task: CancellablePromise, - retryMode: boolean = false - ) => { - const fileUploadTask = dataImportRootStore.fileUploadTasks.find( - ({ name }) => name === fileName - )!; - - // users may click back button in browser - if (isUndefined(fileUploadTask)) { - return; - } - - // the index of fileChunk is pending - dataImportRootStore.mutateFileUploadTasks( - 'pendingChunkIndexes', - [...fileUploadTask.pendingChunkIndexes, fileChunkIndex], - fileName - ); - - let result; - - // cancel all uploads by user - if (fileUploadTask.status === 'failed') { - task.cancel(); - result = undefined; - } else { - result = await task; - } - - if (isUndefined(result) || result.status === 'FAILURE') { - if (fileUploadTask.status !== 'failed') { - if (dataImportRootStore.errorInfo.uploadFiles.message !== '') { - Message.error({ - content: dataImportRootStore.errorInfo.uploadFiles.message, - size: 'medium', - showCloseIcon: false - }); - } - - dataImportRootStore.mutateFileUploadTasks( - 'status', - 'failed', - fileName - ); - } - - dataImportRootStore.mutateFileUploadTasks( - 'failedChunkIndexes', - [...fileUploadTask.failedChunkIndexes, fileChunkIndex], - fileName - ); - - dataImportRootStore.mutateFileUploadTasks( - 'pendingChunkIndexes', - [], - fileName - ); - - dataImportRootStore.removeFileUploadQueue(fileName); - return; - } - - if (result.status === 'SUCCESS') { - dataImportRootStore.mutateFileUploadTasks( - 'uploadedChunkTotal', - fileUploadTask.uploadedChunkTotal + 1, - fileName - ); - - dataImportRootStore.mutateFileUploadTasks( - 'uploadedChunksIndexes', - [...fileUploadTask.uploadedChunksIndexes, fileChunkIndex], - fileName - ); - - if (retryMode) { - // remove failed chunk index after succeed - dataImportRootStore.mutateFileUploadTasks( - 'failedChunkIndexes', - fileUploadTask.failedChunkIndexes.filter( - (failedChunkIndex) => failedChunkIndex !== fileChunkIndex - ), - fileName - ); - - // if there are no longer existed failed chunk index, - // set retry mode to false - if (size(fileUploadTask.failedChunkIndexes) === 0) { - retryMode = false; - } - } - - // all file chunks are uploaded - if (fileUploadTask.chunkTotal === fileUploadTask.uploadedChunkTotal) { - // remove fully uploaded file from queue - dataImportRootStore.removeFileUploadQueue(fileName); - // change the status of fully uploaded file to success - dataImportRootStore.mutateFileUploadTasks( - 'status', - 'success', - fileName - ); - // clear chunkList of fully uploaded file to release memory - dataImportRootStore.mutateFileUploadTasks('chunkList', [], fileName); - - // no uploading files - if ( - dataImportRootStore.fileUploadTasks.every( - ({ status }) => status !== 'uploading' - ) - ) { - dataMapStore.fetchDataMaps(); - return; - } - - if (size(dataImportRootStore.fileRetryUploadList) !== 0) { - // check if there are some failed uploads waiting in retry queue - const fileName = dataImportRootStore.pullRetryFileUploadQueue()!; - const retryFileUploadTask = dataImportRootStore.fileUploadTasks.find( - ({ name }) => name === fileName - )!; - - const task = dataImportRootStore.uploadFiles({ - fileName, - fileChunkList: - retryFileUploadTask.chunkList[ - retryFileUploadTask.failedChunkIndexes[0] - ], - fileChunkTotal: retryFileUploadTask.chunkTotal - }); - - scheduler( - retryFileUploadTask.name, - retryFileUploadTask.failedChunkIndexes[0], - task, - true - ); - - return; - } - - for (const [ - fileIndex, - fileUploadTask - ] of dataImportRootStore.fileUploadTasks.entries()) { - // if there still has files which are fully not being uploaded - if ( - fileUploadTask.uploadedChunkTotal === 0 && - size(fileUploadTask.pendingChunkIndexes) === 0 - ) { - const task = dataImportRootStore.uploadFiles({ - fileName: fileUploadTask.name, - fileChunkList: - dataImportRootStore.fileUploadTasks[fileIndex].chunkList[0], - fileChunkTotal: - dataImportRootStore.fileUploadTasks[fileIndex].chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName: fileUploadTask.name, - status: 'uploading', - task - }); - - scheduler(fileUploadTask.name, 0, task); - - // if queue is full, do not loop to add task - if ( - size(dataImportRootStore.fileUploadQueue) === - MAX_CONCURRENT_UPLOAD - ) { - break; - } - } - } - - return; - } - - let nextUploadChunkIndex; - - if (retryMode) { - const duplicateIndexes = intersection( - fileUploadTask.uploadedChunksIndexes, - fileUploadTask.failedChunkIndexes - ); - - if (!isEmpty(duplicateIndexes)) { - dataImportRootStore.mutateFileUploadTasks( - 'failedChunkIndexes', - fileUploadTask.failedChunkIndexes.filter( - (failedIndex) => !duplicateIndexes.includes(failedIndex) - ), - fileName - ); - } - - if (isEmpty(fileUploadTask.failedChunkIndexes)) { - retryMode = false; - } - - nextUploadChunkIndex = - fileUploadTask.failedChunkIndexes[0] || - Math.max( - ...fileUploadTask.pendingChunkIndexes, - // maybe it just turns retry mode to false - // and the failed chunk index could be less than - // the one which uploads success - // we have to compare uploaded chunk index either - ...fileUploadTask.uploadedChunksIndexes - ) + 1; - } else { - nextUploadChunkIndex = - Math.max( - ...fileUploadTask.pendingChunkIndexes, - // maybe it just turns retry mode to false - // and the failed chunk index could be less than - // the one which uploads success - // we have to compare uploaded chunk index either - ...fileUploadTask.uploadedChunksIndexes - ) + 1; - } - - // remove pending here to get right result of nextUploadChunkIndex - dataImportRootStore.mutateFileUploadTasks( - 'pendingChunkIndexes', - fileUploadTask.pendingChunkIndexes.filter( - (pendingChunkIndex) => pendingChunkIndex !== fileChunkIndex - ), - fileName - ); - - // no recursion - if ( - nextUploadChunkIndex > fileUploadTask.chunkTotal - 1 || - fileUploadTask.uploadedChunksIndexes.includes(nextUploadChunkIndex) - ) { - return; - } - - scheduler( - fileName, - nextUploadChunkIndex, - dataImportRootStore.uploadFiles({ - fileName, - fileChunkList: fileUploadTask.chunkList[nextUploadChunkIndex], - fileChunkTotal: fileUploadTask.chunkTotal - }), - retryMode - ); - - return; - } - }, - [] - ); - - const handleFileDrop = async (monitor: DropTargetMonitor) => { - if (monitor) { - const fileList = monitor.getItem().files; - const currentValidateFileList = validateFileCondition(fileList); - const isFirstBatchUpload = - size(dataImportRootStore.fileUploadTasks) === 0; - - if (isEmpty(currentValidateFileList)) { - return; - } - - await dataImportRootStore.fetchFilehashes( - currentValidateFileList.map(({ name }) => name) - ); - - handleFileChange(currentValidateFileList); - - if (isFirstBatchUpload) { - initFileTaskQueue(); - } else { - const spareQueueItems = - MAX_CONCURRENT_UPLOAD - size(dataImportRootStore.fileUploadQueue); - - if (spareQueueItems === 0) { - return; - } - - // if new selected files are less than spare spaces from queue - if (spareQueueItems >= size(currentValidateFileList)) { - currentValidateFileList.forEach(({ name }) => { - const fileUploadTask = dataImportRootStore.fileUploadTasks.find( - ({ name: fileName }) => fileName === name - )!; - - const task = dataImportRootStore.uploadFiles({ - fileName: name, - fileChunkList: fileUploadTask.chunkList[0], - fileChunkTotal: fileUploadTask.chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName: fileUploadTask.name, - status: 'uploading', - task - }); - scheduler(name, 0, task); - }); - } else { - range(spareQueueItems).forEach((fileUploadTaskIndex) => { - const fileUploadTask = dataImportRootStore.fileUploadTasks.find( - ({ name: fileName }) => - fileName === currentValidateFileList[fileUploadTaskIndex].name - )!; - - const task = dataImportRootStore.uploadFiles({ - fileName: fileUploadTask.name, - fileChunkList: fileUploadTask.chunkList[0], - fileChunkTotal: fileUploadTask.chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName: fileUploadTask.name, - status: 'uploading', - task - }); - scheduler(fileUploadTask.name, 0, task); - }); - } - } - } - }; - - const handleFileSelect = async (e: React.ChangeEvent) => { - // event in async callback should call this - e.persist(); - - if (e.target.files) { - const fileList = Array.from(e.target.files); - const currentValidateFileList = validateFileCondition(fileList); - const isFirstBatchUpload = - size(dataImportRootStore.fileUploadTasks) === 0; - - if (isEmpty(currentValidateFileList)) { - return; - } - - await dataImportRootStore.fetchFilehashes( - currentValidateFileList.map(({ name }) => name) - ); - - handleFileChange(currentValidateFileList); - - if (isFirstBatchUpload) { - initFileTaskQueue(); - } else { - const spareQueueItems = - MAX_CONCURRENT_UPLOAD - size(dataImportRootStore.fileUploadQueue); - - if (spareQueueItems === 0) { - return; - } - - // if new selected files are less than spare spaces from queue - if (spareQueueItems >= size(currentValidateFileList)) { - currentValidateFileList.forEach(({ name }) => { - const fileUploadTask = dataImportRootStore.fileUploadTasks.find( - ({ name: fileName }) => fileName === name - )!; - - const task = dataImportRootStore.uploadFiles({ - fileName: name, - fileChunkList: fileUploadTask.chunkList[0], - fileChunkTotal: fileUploadTask.chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName: fileUploadTask.name, - status: 'uploading', - task - }); - scheduler(name, 0, task); - }); - } else { - range(spareQueueItems).forEach((fileUploadTaskIndex) => { - const fileUploadTask = dataImportRootStore.fileUploadTasks.find( - ({ name: fileName }) => - fileName === currentValidateFileList[fileUploadTaskIndex].name - )!; - - const task = dataImportRootStore.uploadFiles({ - fileName: fileUploadTask.name, - fileChunkList: fileUploadTask.chunkList[0], - fileChunkTotal: fileUploadTask.chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName: fileUploadTask.name, - status: 'uploading', - task - }); - scheduler(fileUploadTask.name, 0, task); - }); - } - } - } - - // select same file will not trigger onChange on input[type="file"] - // need to reset its value here - e.target.value = ''; - }; - - const dragAreaClassName = classnames({ - 'import-tasks-upload-drag-area': true, - 'file-above': canDrop && isOver - }); - - // if upload file api throw errors - useEffect(() => { - if (dataImportRootStore.errorInfo.uploadFiles.message !== '') { - Message.error({ - content: dataImportRootStore.errorInfo.uploadFiles.message, - size: 'medium', - showCloseIcon: false - }); - } - }, [dataImportRootStore.errorInfo.uploadFiles.message]); - - return ( -
    - - -
    - ); -}); - -export interface FileListProps { - scheduler: ( - fileName: string, - fileChunkIndex: number, - task: CancellablePromise, - retryMode?: boolean - ) => Promise; -} - -export const FileList: React.FC = observer(({ scheduler }) => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore } = dataImportRootStore; - - const handleRetry = (fileName: string) => () => { - // if (size(dataImportRootStore.fileUploadQueue) === 0) { - if (size(dataImportRootStore.fileUploadQueue) < MAX_CONCURRENT_UPLOAD) { - const fileUploadTask = dataImportRootStore.fileUploadTasks.find( - ({ name }) => name === fileName - )!; - - const task = dataImportRootStore.uploadFiles({ - fileName, - fileChunkList: - fileUploadTask.chunkList[fileUploadTask.failedChunkIndexes[0]] || - fileUploadTask.chunkList[ - Math.max( - ...fileUploadTask.pendingChunkIndexes, - // maybe it just turns retry mode to false - // and the failed chunk index could be less than - // the one which uploads success - // we have to compare uploaded chunk index either - ...fileUploadTask.uploadedChunksIndexes - ) + 1 - ], - fileChunkTotal: fileUploadTask.chunkTotal - }); - - dataImportRootStore.addFileUploadQueue({ - fileName, - status: 'uploading', - task - }); - - dataImportRootStore.mutateFileUploadTasks( - 'status', - 'uploading', - fileName - ); - - if (!isEmpty(fileUploadTask.failedChunkIndexes)) { - dataImportRootStore.mutateFileUploadTasks( - 'failedChunkIndexes', - [...fileUploadTask.failedChunkIndexes.slice(1)], - fileName - ); - } - - scheduler( - fileName, - fileUploadTask.failedChunkIndexes[0] || - Math.max( - ...fileUploadTask.pendingChunkIndexes, - // maybe it just turns retry mode to false - // and the failed chunk index could be less than - // the one which uploads success - // we have to compare uploaded chunk index either - ...fileUploadTask.uploadedChunksIndexes - ) + 1, - task, - true - ); - } else { - // or just add filename to retry queue, - // let other scheduler decide when to call it - dataImportRootStore.addRetryFileUploadQueue(fileName); - } - }; - - const handleDelete = (name: string, existed = false) => async () => { - await dataImportRootStore.deleteFiles(name); - - if (dataImportRootStore.requestStatus.deleteFiles === 'failed') { - Message.error({ - content: dataImportRootStore.errorInfo.deleteFiles.message, - size: 'medium', - showCloseIcon: false - }); - - return; - } - - if (existed === true) { - dataMapStore.fetchDataMaps(); - return; - } - - dataImportRootStore.removeFileUploadTasks(name); - - // if no uploading files, fetch data maps again - if ( - !dataImportRootStore.fileUploadTasks.some( - ({ status }) => status === 'uploading' - ) - ) { - dataMapStore.fetchDataMaps(); - } - }; - - return ( -
    - {dataImportRootStore.fileUploadTasks.map( - ({ name, size, chunkTotal, uploadedChunkTotal, status }) => { - const [sizeKB, sizeMB, sizeGB] = [size / KB, size / MB, size / GB]; - const convertedSize = - sizeGB > 1 - ? String(sizeGB.toFixed(2)) + ' GB' - : sizeMB > 1 - ? String(sizeMB.toFixed(2)) + ' MB' - : sizeKB > 1 - ? String(sizeKB.toFixed(2)) + ' KB' - : String(size) + ' Byte'; - const progress = Number( - ((uploadedChunkTotal / chunkTotal) * 100).toFixed(2) - ); - - return ( -
    -
    - {name} - {convertedSize} -
    -
    - - {status === 'failed' && progress !== 100 && ( - retry-upload-file - )} - delete-file -
    -
    - ); - } - )} - {dataMapStore.fileMapInfos - .filter( - ({ name }) => - !dataImportRootStore.successFileUploadTaskNames.includes(name) - ) - .map(({ name, total_size }) => { - return ( -
    -
    - {name} - {total_size} -
    -
    - - delete-file -
    -
    - ); - })} -
    - ); -}); - -export default UploadEntry; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/DataMapConfigs.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/DataMapConfigs.less deleted file mode 100644 index 174042063..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/DataMapConfigs.less +++ /dev/null @@ -1,393 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.import-tasks { - &-data-map { - margin-bottom: 40px; - } - - &-data-map-tooltip { - background: #fff; - box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2); - border-radius: 4px; - padding: 16px; - font-size: 14px; - - &-text { - margin-bottom: 8px; - - &:nth-child(2) { - color: #e64552; - font-size: 16px; - margin-bottom: 12px; - } - - &:last-child { - margin-bottom: 0; - } - } - - &.no-display { - display: none; - } - } - - &-data-type-info-wrapper { - // margin-bottom: 16px; - padding: 24px 16px; - height: 100px; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - display: flex; - position: relative; - } - - &-data-type-info { - display: flex; - flex-direction: column; - padding-right: 8px; - // flex: 1; - - &:nth-child(1) { - // width: 24%; - flex: 1 0; - - & img.collpase { - transform: rotate(-90deg); - transition: transform 0.3s; - } - - & img.expand { - transform: rotate(0deg); - transition: transform 0.3s; - } - } - - &:nth-child(2) { - flex: 1 0; - } - - &:nth-child(3) { - flex: 1 0; - } - - &:last-child { - padding-right: 0; - width: 168px; - flex-direction: row; - align-items: center; - } - - & > div { - display: flex; - } - - &-title { - font-size: 12px; - line-height: 18px; - color: #666; - } - - &-content { - max-width: 180px; - margin-top: 13px; - font-size: 16px; - line-height: 22px; - color: #333; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - &-data-options { - display: flex; - line-height: 32px; - margin-bottom: 32px; - font-size: 14px; - align-items: center; - - &-title { - display: flex; - justify-content: flex-end; - align-items: center; - width: 62px; - margin-right: 44px; - color: #333; - font-size: 14px; - - &.in-card { - width: 76px; - } - } - - &-value-maps { - width: 650px; - background-color: #f5f5f5; - margin-bottom: 16px; - padding: 16px 40px; - } - - &-expand-table { - width: 80%; - display: flex; - flex-direction: column; - - &-row { - display: flex; - margin-bottom: 8px; - - &:first-child { - margin-bottom: 12px; - } - } - - &-column { - height: 32px; - display: flex; - align-items: center; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 14px; - line-height: 32px; - - &:nth-child(1) { - flex-basis: 30%; - } - - &:nth-child(2) { - flex-basis: 30%; - } - - &:nth-child(3) { - flex-basis: 30%; - } - - &:last-child { - flex-basis: 10%; - justify-content: center; - } - } - } - - &-expand-dropdown { - width: 382px; - max-height: 328px; - overflow: auto; - background: #fff; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - color: #333; - - & > div { - padding: 0 16px; - height: 32px; - display: flex; - align-items: center; - } - } - - &-expand-values { - display: flex; - flex-direction: column; - } - - &-expand-value { - display: flex; - margin-bottom: 32px; - - &:nth-last-child(2) { - margin-bottom: 16px; - } - - &:nth-last-child(1) { - margin-bottom: 0; - } - - & img { - margin: 0 2px; - } - - &-column { - height: 32px; - display: flex; - align-items: center; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 14px; - line-height: 32px; - - &:nth-child(1) { - width: 30%; - } - - &:nth-child(2) { - width: 30%; - } - - &:nth-child(3) { - width: 30%; - } - - &:nth-child(4) { - width: 10%; - } - } - } - - &-expand-info { - display: flex; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - & > span { - font-size: 14px; - line-height: 32px; - - &:nth-child(1) { - width: 60px; - } - } - - & > img { - margin: 0 6px; - } - } - - &-expand-input { - margin-top: 12px; - - &:first-child { - margin-top: 0; - } - } - } - - &-data-type-manipulations { - display: flex; - margin-left: auto; - } - - &-data-map-manipulations { - width: 100%; - margin-top: 40px; - display: flex; - justify-content: center; - } - - &-data-map-configs { - padding: 0 16px; - width: 100%; - overflow: auto; - } - - // &-data-map-config-header { - // display: flex; - // margin-bottom: 16px; - - // & > span { - // font-weight: 900; - // line-height: 24px; - // } - - // & img { - // cursor: pointer; - // } - - // &-expand { - // margin-left: 9.7px; - // transition: transform 0.3s; - // } - - // &-collpase { - // margin-left: 9.7px; - // transform: rotate(-180deg); - // transition: transform 0.3s; - // } - // } - - &-data-map-config-card { - // prevent box-shadow being cliped - // margin: 0 3px; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - margin-bottom: 16px; - padding: 16px; - } - - &-data-map-config-view { - width: 100%; - background: #fafafa; - border: 1px solid #eee; - border-radius: 0 0 3px 3px; - margin-bottom: 16px; - padding: 16px; - // position: absolute; - // top: 100px; - // left: 0; - z-index: 9; - } -} - -.import-tasks-tooltips { - width: 368px; - background: #fff; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - padding: 24px; - font-size: 14px; - color: #333; - - & p { - margin: 0; - - &:nth-of-type(1) { - margin-bottom: 16px; - font-weight: 900; - font-size: 16px; - color: #000; - } - - &:nth-of-type(2) { - margin-bottom: 24px; - font-size: 14px; - color: #333; - } - } -} - -/* override */ - -.import-tasks-data-options - .new-fc-one-checkbox-group.new-fc-one-checkbox-group-medium.new-fc-one-checkbox-group-row { - line-height: normal; -} - -// reveal scrollbar by auto, directly override style on will both set internal wrapper and
      itself, which cause shrinks on . -.import-tasks-step-wrapper - > .new-fc-one-menu-inline-box.new-fc-one-menu-inline-medium - > ul { - overflow: auto; -} - -.import-tasks-data-map-configs - .new-fc-one-input-error.new-fc-one-input-error-layer { - word-break: keep-all; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/DataMapConfigs.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/DataMapConfigs.tsx deleted file mode 100644 index 562fac5aa..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/DataMapConfigs.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { isEmpty } from 'lodash-es'; -import { Menu } from 'hubble-ui'; - -import { - ImportManagerStoreContext, - DataImportRootStoreContext -} from '../../../../../stores'; -import FileConfigs from './FileConfigs'; -import TypeConfigs from './TypeConfigs'; -import { useInitDataImport } from '../../../../../hooks'; - -import './DataMapConfigs.less'; - -export interface DataMapConfigsProps { - height?: string; -} - -const DataMapConfigs: React.FC = observer(({ height }) => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const isInitReady = useInitDataImport(); - const realHeight = height ? height : 'calc(100vh - 194px)'; - - return isInitReady ? ( -
      - { - // reset state from the previous file - dataMapStore.resetDataMaps(); - - // if data import starts, do not expand collpase - if (!serverDataImportStore.isServerStartImport) { - dataMapStore.switchExpand('file', true); - } - - dataMapStore.setSelectedFileId(Number(e.key)); - dataMapStore.setSelectedFileInfo(); - serverDataImportStore.switchImporting(false); - }} - > - {dataMapStore.fileMapInfos - .filter(({ file_status }) => file_status === 'COMPLETED') - .map(({ id, name }) => ( - - {name} - - ))} - -
      - - -
      -
      - ) : null; -}); - -export default DataMapConfigs; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/EdgeMap.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/EdgeMap.tsx deleted file mode 100644 index 84729a65a..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/EdgeMap.tsx +++ /dev/null @@ -1,1707 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useState } from 'react'; -import { observer } from 'mobx-react'; -import { isUndefined, isEmpty, size, cloneDeep } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Input, Select, Checkbox, Message } from 'hubble-ui'; - -import { Tooltip } from '../../../../common'; -import { DataImportRootStoreContext } from '../../../../../stores'; -import { - VertexType, - EdgeType -} from '../../../../../stores/types/GraphManagementStore/metadataConfigsStore'; -import TypeConfigManipulations from './TypeConfigManipulations'; -import { getUnicodeLength } from '../../../../../utils'; - -import ArrowIcon from '../../../../../assets/imgs/ic_arrow_16.svg'; -import BlueArrowIcon from '../../../../../assets/imgs/ic_arrow_blue.svg'; -import CloseIcon from '../../../../../assets/imgs/ic_close_16.svg'; -import MapIcon from '../../../../../assets/imgs/ic_yingshe_16.svg'; - -export interface EdgeMapProps { - checkOrEdit: 'check' | 'edit' | boolean; - onCancelCreateEdge: () => void; - edgeMapIndex?: number; -} - -const EdgeMap: React.FC = observer( - ({ checkOrEdit, onCancelCreateEdge, edgeMapIndex }) => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore } = dataImportRootStore; - const [isAddMapping, switchAddMapping] = useState(false); - const [isExpandAdvance, switchExpandAdvance] = useState(false); - const { t } = useTranslation(); - - const isCheck = checkOrEdit === 'check'; - const isEdit = checkOrEdit === 'edit'; - const edgeMap = - checkOrEdit !== false - ? dataMapStore.editedEdgeMap! - : dataMapStore.newEdgeType; - const filteredColumnNamesInSelection = - checkOrEdit !== false - ? dataMapStore.filteredColumnNamesInEdgeEditSelection - : dataMapStore.filteredColumnNamesInEdgeNewSelection; - - const findEdge = (collection: EdgeType[], label: string) => - collection.find(({ name }) => name === label); - - const findVertex = (collection: VertexType[], label: string) => - collection.find(({ name }) => name === label); - - const selectedEdge = findEdge(dataImportRootStore.edgeTypes, edgeMap.label); - - let selectedSourceVertex: VertexType | undefined; - let selectedTargetVertex: VertexType | undefined; - - if (!isUndefined(selectedEdge)) { - selectedSourceVertex = findVertex( - dataImportRootStore.vertexTypes, - selectedEdge.source_label - ); - selectedTargetVertex = findVertex( - dataImportRootStore.vertexTypes, - selectedEdge.target_label - ); - } - - const isStrategyAutomatic = - selectedSourceVertex?.id_strategy === 'AUTOMATIC' || - selectedTargetVertex?.id_strategy === 'AUTOMATIC'; - - const handleExpand = () => { - dataMapStore.switchExpand('type', !dataMapStore.isExpandTypeConfig); - }; - - const handleExpandAdvance = () => { - switchExpandAdvance(!isExpandAdvance); - }; - - const wrapperName = classnames({ - 'import-tasks-data-map-config-card': !Boolean(checkOrEdit), - 'import-tasks-data-map-config-view': Boolean(checkOrEdit) - }); - - const expandAdvanceClassName = classnames({ - 'import-tasks-step-content-header-expand': isExpandAdvance, - 'import-tasks-step-content-header-collpase': !isExpandAdvance - }); - - const expandAddMapClassName = classnames({ - 'import-tasks-step-content-header-expand': isAddMapping, - 'import-tasks-step-content-header-collpase': !isAddMapping - }); - - const addMappingManipulationClassName = classnames({ - 'import-tasks-manipulation': true, - 'import-tasks-manipulation-disabled': - size(filteredColumnNamesInSelection) === 0 || isStrategyAutomatic - }); - - const addNullValueClassName = classnames({ - 'import-tasks-manipulation': true, - 'import-tasks-manipulation-disabled': edgeMap.null_values.customized.includes( - '' - ) - }); - - const addPropertyMapClassName = classnames({ - 'import-tasks-manipulation': true, - 'import-tasks-manipulation-disabled': - !dataMapStore.allowAddPropertyMapping('edge') || isStrategyAutomatic - }); - - return ( -
      - {Boolean(checkOrEdit) ? ( -
      - {t('data-configs.type.basic-settings')} -
      - ) : ( -
      - {t('data-configs.type.edge.title')} - collpaseOrExpand -
      - )} -
      - - {t('data-configs.type.edge.type')}: - - {isCheck ? ( - {dataMapStore.editedEdgeMap!.label} - ) : ( - - )} -
      - - {!isUndefined(selectedSourceVertex) && ( -
      - - {t('data-configs.type.edge.source-ID-strategy')}: - - - {t('data-configs.type.hint.lack-support-for-automatic')} - - } - > - {t( - `data-configs.type.ID-strategy.${selectedSourceVertex?.id_strategy}` - )} - {selectedSourceVertex?.id_strategy === 'PRIMARY_KEY' && - `-${selectedSourceVertex?.primary_keys.join(',')}`} - -
      - )} - {edgeMap.source_fields.map((idField, fieldIndex) => { - return ( -
      - - {t('data-configs.type.edge.ID-column') + - (selectedSourceVertex?.id_strategy === 'PRIMARY_KEY' - ? fieldIndex + 1 - : '')} - : - - {isCheck ? ( - {idField} - ) : ( - - )} -
      - ); - })} - - {!isUndefined(selectedTargetVertex) && ( -
      - - {t('data-configs.type.edge.target-ID-strategy')}: - - - {t('data-configs.type.hint.lack-support-for-automatic')} - - } - > - {t( - `data-configs.type.ID-strategy.${selectedTargetVertex?.id_strategy}` - )} - {selectedTargetVertex?.id_strategy === 'PRIMARY_KEY' && - `-${selectedTargetVertex?.primary_keys.join(',')}`} - -
      - )} - {edgeMap.target_fields.map((idField, fieldIndex) => { - return ( -
      - - {t('data-configs.type.edge.ID-column') + - (selectedTargetVertex?.id_strategy === 'PRIMARY_KEY' - ? fieldIndex + 1 - : '')} - : - - {isCheck ? ( - {idField} - ) : ( - - )} -
      - ); - })} - -
      - - {t('data-configs.type.edge.map-settings')}: - -
      - {isCheck && - isEmpty(dataMapStore.editedEdgeMap?.field_mapping) && - '-'} - - {((Boolean(checkOrEdit) === false && - !isEmpty(dataMapStore.newEdgeType.field_mapping)) || - (Boolean(checkOrEdit) !== false && - !isEmpty(dataMapStore.editedEdgeMap!.field_mapping))) && ( -
      -
      - {t('data-configs.type.edge.add-map.name')} -
      -
      - {t('data-configs.type.edge.add-map.sample')} -
      -
      - {t('data-configs.type.edge.add-map.property')} -
      -
      - {!isCheck && ( - { - isEdit - ? dataMapStore.toggleEdgeSelectAllFieldMapping( - 'edit', - false, - selectedEdge - ) - : dataMapStore.toggleEdgeSelectAllFieldMapping( - 'new', - false, - selectedEdge - ); - }} - > - {t('data-configs.type.edge.add-map.clear')} - - )} -
      -
      - )} - {edgeMap.field_mapping.map( - ({ column_name, mapped_name }, fieldIndex) => { - const param = checkOrEdit === false ? 'new' : 'edit'; - - return ( -
      -
      - {column_name} -
      -
      - { - dataMapStore.selectedFileInfo?.file_setting - .column_values[ - dataMapStore.selectedFileInfo?.file_setting.column_names.findIndex( - (name) => column_name === name - ) - ] - } -
      -
      - {isCheck ? ( - {mapped_name} - ) : ( - - )} -
      -
      - {!isCheck && ( - close { - dataMapStore.removeEdgeFieldMapping( - param, - column_name - ); - }} - /> - )} -
      -
      - ); - } - )} - - {!isCheck && ( - <> -
      { - if ( - size(filteredColumnNamesInSelection) === 0 || - isStrategyAutomatic - ) { - switchAddMapping(false); - return; - } - - switchAddMapping(!isAddMapping); - }} - > -
      - {t('data-configs.type.edge.add-map.title')} -
      - expand -
      - {isAddMapping && ( -
      -
      - - { - if (isEdit) { - const isIndeterminate = - !isEmpty( - dataMapStore.editedEdgeMap?.field_mapping - ) && - size( - dataMapStore.editedEdgeMap?.field_mapping - ) !== - size( - dataMapStore.filteredColumnNamesInEdgeEditSelection - ); - - dataMapStore.toggleEdgeSelectAllFieldMapping( - 'edit', - // if isIndeterminate is true, e.target.checked is false - isIndeterminate || e.target.checked, - selectedEdge - ); - } else { - const isIndeterminate = - !isEmpty( - dataMapStore.newEdgeType.field_mapping - ) && - size(dataMapStore.newEdgeType.field_mapping) !== - size( - dataMapStore.filteredColumnNamesInEdgeNewSelection - ); - - dataMapStore.toggleEdgeSelectAllFieldMapping( - 'new', - isIndeterminate || e.target.checked, - selectedEdge - ); - } - }} - > - {t('data-configs.type.edge.select-all')} - - -
      - {filteredColumnNamesInSelection.map((name) => { - const param = isEdit ? 'edit' : 'new'; - - let combinedText = name; - let currentColumnValue = dataMapStore.selectedFileInfo - ?.file_setting.column_values[ - dataMapStore.selectedFileInfo?.file_setting.column_names.findIndex( - (columnName) => name === columnName - ) - ] as string; - combinedText += `(${currentColumnValue})`; - - if (getUnicodeLength(combinedText) > 35) { - combinedText = combinedText.slice(0, 35) + '...'; - } - - const mappingValue = selectedEdge?.properties.find( - ({ name: propertyName }) => propertyName === name - )?.name; - - return ( -
      - - column_name === name - ) - ) - } - onChange={(e: any) => { - if (e.target.checked) { - dataMapStore.setEdgeFieldMappingKey( - param, - name, - mappingValue - ); - } else { - dataMapStore.removeEdgeFieldMapping( - param, - name - ); - } - }} - > - {combinedText} - - -
      - ); - })} -
      - )} - - )} -
      -
      - -
      - {t('data-configs.type.edge.advance.title')} - {!isCheck && ( - collpaseOrExpand - )} -
      - {(isExpandAdvance || isCheck) && ( - <> -
      - - {t('data-configs.type.edge.advance.nullable-list.title')}: - - {isCheck ? ( - - {dataMapStore - .editedEdgeMap!.null_values.checked.filter( - (value) => value !== 'null' - ) - .map((value) => - value === '' - ? t('addition.common.null-value') - : value === 'NULL' - ? 'NULL/null' - : value - ) - .concat(dataMapStore.editedEdgeMap!.null_values.customized) - .join(',')} - - ) : ( - <> -
      - { - isEdit - ? dataMapStore.editCheckedNullValues( - 'edit', - 'edge', - checkedList - ) - : dataMapStore.editCheckedNullValues( - 'new', - 'edge', - checkedList - ); - }} - value={ - isEdit - ? dataMapStore.editedEdgeMap!.null_values.checked - : dataMapStore.newEdgeType.null_values.checked - } - > - NULL/null - - {t( - 'data-configs.type.edge.advance.nullable-list.empty' - )} - - -
      - { - dataMapStore.toggleCustomNullValue( - isEdit ? 'edit' : 'new', - 'edge', - e.target.checked - ); - - if (e.target.checked) { - dataMapStore.addValidateValueMappingOption( - 'null_values' - ); - } else { - dataMapStore.resetValidateValueMapping( - 'null_values' - ); - } - }} - > - {t( - 'data-configs.type.edge.advance.nullable-list.custom' - )} - -
      -
      -
      - {isEdit ? ( - <> - {dataMapStore.editedEdgeMap?.null_values.customized.map( - (nullValue, nullValueIndex) => ( -
      - { - dataMapStore.editCustomNullValues( - 'edit', - 'edge', - e.value, - nullValueIndex - ); - - dataMapStore.validateValueMapping( - 'edge', - 'edit', - 'null_values', - nullValueIndex - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore.validateAdvanceConfigErrorMessage - .null_values[nullValueIndex] - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'edge', - 'edit', - 'null_values', - nullValueIndex - ); - } - }} - /> -
      - ) - )} - {!isEmpty( - dataMapStore.editedEdgeMap?.null_values.customized - ) && ( -
      - { - const extraNullValues = - dataMapStore.editedEdgeMap?.null_values - .customized; - - if (!extraNullValues?.includes('')) { - dataMapStore.addCustomNullValues( - 'edit', - 'edge' - ); - } - }} - > - {t('data-configs.manipulations.add')} - -
      - )} - - ) : ( - <> - {dataMapStore.newEdgeType.null_values.customized.map( - (nullValue, nullValueIndex) => ( -
      - { - dataMapStore.editCustomNullValues( - 'new', - 'edge', - e.value, - nullValueIndex - ); - - dataMapStore.validateValueMapping( - 'edge', - 'new', - 'null_values', - nullValueIndex - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore.validateAdvanceConfigErrorMessage - .null_values[nullValueIndex] - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'edge', - 'new', - 'null_values', - nullValueIndex - ); - } - }} - /> -
      - ) - )} - {!isEmpty( - dataMapStore.newEdgeType.null_values.customized - ) && ( -
      - { - const extraNullValues = - dataMapStore.newEdgeType?.null_values - .customized; - - if (!extraNullValues.includes('')) { - dataMapStore.addCustomNullValues( - 'new', - 'edge' - ); - } - }} - > - {t('data-configs.manipulations.add')} - -
      - )} - - )} -
      - - )} -
      - -
      - - {t('data-configs.type.edge.advance.map-property-value.title')}: - - {!isCheck && - (isEdit - ? isEmpty(dataMapStore.editedEdgeMap?.value_mapping) - : isEmpty(dataMapStore.newEdgeType.value_mapping)) && ( -
      { - if (isStrategyAutomatic) { - return; - } - - isEdit - ? dataMapStore.addEdgeValueMapping('edit') - : dataMapStore.addEdgeValueMapping('new'); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings' - ); - }} - > - {t( - 'data-configs.type.edge.advance.map-property-value.add-value' - )} -
      - )} - - {isCheck && ( -
      - {isEmpty(dataMapStore.editedEdgeMap?.value_mapping) - ? '-' - : dataMapStore.editedEdgeMap?.value_mapping.map( - ({ column_name, values }, valueMapIndex) => ( -
      -
      - - {t( - 'data-configs.type.edge.advance.map-property-value.fields.property' - )} - {valueMapIndex + 1}: - - {column_name} -
      - {values.map(({ column_value, mapped_value }) => ( -
      - - {t( - 'data-configs.type.edge.advance.map-property-value.fields.value-map' - )} - - {column_value} - map - {mapped_value} -
      - ))} -
      - ) - )} -
      - )} -
      - - {/* property value mapping form */} - {!isCheck && - (!Boolean(checkOrEdit) - ? dataMapStore.newEdgeType.value_mapping.map( - ({ column_name, values }, valueMapIndex) => ( -
      -
      - - {t( - 'data-configs.type.edge.advance.map-property-value.fields.property' - )} - {valueMapIndex + 1}: - - - { - dataMapStore.removeEdgeValueMapping( - 'new', - valueMapIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'edge', - 'new', - 'value_mappings', - valueMapIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - -
      -
      - - {t( - 'data-configs.type.edge.advance.map-property-value.fields.value-map' - )} - : - -
      - {values.map( - ({ column_value, mapped_value }, valueIndex) => ( -
      - { - dataMapStore.editEdgeValueMappingColumnValueName( - 'new', - 'column_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'edge', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].column_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'edge', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - } - }} - /> - map - { - dataMapStore.editEdgeValueMappingColumnValueName( - 'new', - 'mapped_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'edge', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].mapped_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'edge', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - } - }} - /> - {values.length > 1 && ( - { - dataMapStore.removeEdgeValueMappingValue( - 'new', - valueMapIndex, - valueIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'edge', - 'new', - 'value_mappings_value', - valueMapIndex, - valueIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - - )} -
      - ) - )} -
      -
      - { - if ( - !dataMapStore.allowAddPropertyValueMapping( - 'edge', - valueMapIndex - ) - ) { - return; - } - - dataMapStore.addEdgeValueMappingValue( - 'new', - valueMapIndex - ); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings_value', - valueMapIndex - ); - }} - > - {t( - 'data-configs.type.edge.advance.map-property-value.fields.add-value-map' - )} - -
      -
      -
      -
      -
      - ) - ) - : dataMapStore.editedEdgeMap?.value_mapping.map( - ({ column_name, values }, valueMapIndex) => ( -
      -
      - - {t( - 'data-configs.type.edge.advance.map-property-value.fields.property' - )} - {valueMapIndex + 1}: - - - { - dataMapStore.removeEdgeValueMapping( - 'edit', - valueMapIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'edge', - 'edit', - 'value_mappings', - valueMapIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - -
      -
      - - {t( - 'data-configs.type.edge.advance.map-property-value.fields.value-map' - )} - : - -
      - {values.map( - ({ column_value, mapped_value }, valueIndex) => ( -
      - { - dataMapStore.editEdgeValueMappingColumnValueName( - 'edit', - 'column_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'edge', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].column_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'edge', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - } - }} - /> - map - { - dataMapStore.editEdgeValueMappingColumnValueName( - 'edit', - 'mapped_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'edge', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].mapped_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'edge', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - } - }} - /> - {values.length > 1 && ( - { - dataMapStore.removeEdgeValueMappingValue( - 'edit', - valueMapIndex, - valueIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'edge', - 'edit', - 'value_mappings_value', - valueMapIndex, - valueIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - - )} -
      - ) - )} -
      -
      - { - if ( - !dataMapStore.allowAddPropertyValueMapping( - 'edge', - valueMapIndex - ) - ) { - return; - } - - dataMapStore.addEdgeValueMappingValue( - 'edit', - valueMapIndex - ); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings_value', - valueMapIndex - ); - }} - > - {t( - 'data-configs.type.edge.advance.map-property-value.fields.add-value-map' - )} - -
      -
      -
      -
      -
      - ) - ))} - - {!isCheck && - (isEdit - ? !isEmpty(dataMapStore.editedEdgeMap?.value_mapping) - : !isEmpty(dataMapStore.newEdgeType.value_mapping)) && ( -
      { - if (!dataMapStore.allowAddPropertyMapping('edge')) { - return; - } - - isEdit - ? dataMapStore.addEdgeValueMapping('edit') - : dataMapStore.addEdgeValueMapping('new'); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings' - ); - }} - > - {t( - 'data-configs.type.edge.advance.map-property-value.add-value' - )} -
      - )} - - )} - - {!isCheck && ( - - isEmpty(mapped_name) - ) || - !dataMapStore.allowAddPropertyMapping('edge') || - !dataMapStore.isValidateSave - } - onCreate={async () => { - dataMapStore.switchAddNewTypeConfig(false); - dataMapStore.switchEditTypeConfig(false); - - isEdit - ? await dataMapStore.updateEdgeMap( - 'upgrade', - dataMapStore.selectedFileId - ) - : await dataMapStore.updateEdgeMap( - 'add', - dataMapStore.selectedFileId - ); - - if (dataMapStore.requestStatus.updateEdgeMap === 'failed') { - Message.error({ - content: dataMapStore.errorInfo.updateEdgeMap.message, - size: 'medium', - showCloseIcon: false - }); - } - - onCancelCreateEdge(); - dataMapStore.resetNewMap('edge'); - }} - onCancel={() => { - dataMapStore.switchAddNewTypeConfig(false); - dataMapStore.switchEditTypeConfig(false); - - if (!isEdit) { - dataMapStore.resetNewMap('edge'); - } else { - dataMapStore.resetEditMapping('edge'); - } - - onCancelCreateEdge(); - dataMapStore.resetNewMap('edge'); - dataMapStore.resetValidateValueMapping('all'); - }} - /> - )} -
      - ); - } -); - -export default EdgeMap; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/FileConfigs.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/FileConfigs.tsx deleted file mode 100644 index d8f9cc00f..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/FileConfigs.tsx +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { range, rangeRight } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Radio, Switch, Input, Select, Button, Message } from 'hubble-ui'; - -import { DataImportRootStoreContext } from '../../../../../stores'; - -import ArrowIcon from '../../../../../assets/imgs/ic_arrow_16.svg'; - -const separators = [',', ';', '\\t', ' ']; -const charsets = ['UTF-8', 'GBK', 'ISO-8859-1', 'US-ASCII']; -const dateFormat = [ - 'yyyy-MM-dd', - 'yyyy-MM-dd HH:mm:ss', - 'yyyy-MM-dd HH:mm:ss.SSS' -]; - -const timezones = rangeRight(1, 13) - .map((num) => `GMT-${num}`) - .concat(['GMT']) - .concat(range(1, 13).map((num) => `GMT+${num}`)); - -const styles = { - smallGap: { - marginBottom: 18 - }, - mediumGap: { - marginBottom: 23 - } -}; - -const FileConfigs: React.FC = observer(() => { - const { dataMapStore, serverDataImportStore } = useContext( - DataImportRootStoreContext - ); - const { t } = useTranslation(); - - const expandClassName = classnames({ - 'import-tasks-step-content-header-expand': dataMapStore.isExpandFileConfig, - 'import-tasks-step-content-header-collpase': !dataMapStore.isExpandFileConfig - }); - - const handleExpand = () => { - dataMapStore.switchExpand('file', !dataMapStore.isExpandFileConfig); - }; - - return ( - dataMapStore.selectedFileInfo && ( -
      -
      - {t('data-configs.file.title')} - collpaseOrExpand -
      - {dataMapStore.isExpandFileConfig && ( - <> -
      - - {t('data-configs.file.include-header')}: - - { - dataMapStore.setFileConfig('has_header', checked); - }} - /> -
      -
      - - {t('data-configs.file.delimiter.title')}: - -
      - ) => { - if (e.target.value === 'custom') { - dataMapStore.setFileConfig('delimiter', ''); - } else { - dataMapStore.setFileConfig('delimiter', e.target.value); - // reset validate error message of delimiter - dataMapStore.validateFileInfo('delimiter'); - } - }} - > - - {t('data-configs.file.delimiter.comma')} - - - {t('data-configs.file.delimiter.semicolon')} - - - {t('data-configs.file.delimiter.tab')} - - - {t('data-configs.file.delimiter.space')} - - - {t('data-configs.file.delimiter.custom')} - - -
      - {!separators.includes( - dataMapStore.selectedFileInfo!.file_setting.delimiter - ) && ( -
      - { - dataMapStore.setFileConfig('delimiter', e.value); - dataMapStore.validateFileInfo('delimiter'); - }} - errorMessage={ - dataMapStore.validateFileInfoErrorMessage.delimiter - } - errorLocation="layer" - originInputProps={{ - onBlur: () => { - dataMapStore.validateFileInfo('delimiter'); - } - }} - /> -
      - )} -
      -
      - - {t('data-configs.file.code-type.title')}: - -
      - ) => { - if (e.target.value === 'custom') { - dataMapStore.setFileConfig('charset', ''); - } else { - dataMapStore.setFileConfig('charset', e.target.value); - dataMapStore.validateFileInfo('charset'); - } - }} - > - - {t('data-configs.file.code-type.UTF-8')} - - - {t('data-configs.file.code-type.GBK')} - - - {t('data-configs.file.code-type.ISO-8859-1')} - - - {t('data-configs.file.code-type.US-ASCII')} - - - {t('data-configs.file.code-type.custom')} - - -
      - {!charsets.includes( - dataMapStore.selectedFileInfo!.file_setting.charset - ) && ( -
      - { - dataMapStore.setFileConfig('charset', e.value); - dataMapStore.validateFileInfo('charset'); - }} - errorMessage={ - dataMapStore.validateFileInfoErrorMessage.charset - } - errorLocation="layer" - originInputProps={{ - onBlur: () => { - dataMapStore.validateFileInfo('charset'); - } - }} - /> -
      - )} -
      -
      - - {t('data-configs.file.date-type.title')}: - -
      - ) => { - if (e.target.value === 'custom') { - dataMapStore.setFileConfig('date_format', ''); - } else { - dataMapStore.setFileConfig('date_format', e.target.value); - dataMapStore.validateFileInfo('date_format'); - } - }} - > - yyyy-MM-dd - yyyy-MM-dd HH:MM:SS - - yyyy-MM-dd HH:mm:ss.SSS - - - {t('data-configs.file.code-type.custom')} - - - {!dateFormat.includes( - dataMapStore.selectedFileInfo!.file_setting.date_format - ) && ( -
      - { - dataMapStore.setFileConfig('date_format', e.value); - dataMapStore.validateFileInfo('date_format'); - }} - errorMessage={ - dataMapStore.validateFileInfoErrorMessage.date_format - } - errorLocation="layer" - originInputProps={{ - onBlur: () => { - dataMapStore.validateFileInfo('date_format'); - } - }} - /> -
      - )} -
      -
      -
      - - {t('data-configs.file.skipped-line')}: - - { - dataMapStore.setFileConfig('skipped_line', e.value); - dataMapStore.validateFileInfo('skipped_line'); - }} - errorLocation="layer" - errorMessage={ - dataMapStore.validateFileInfoErrorMessage.skipped_line - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateFileInfo('skipped_line'); - } - }} - /> -
      -
      - - {t('data-configs.file.timezone')}: - - -
      - {!dataMapStore.readOnly && !dataMapStore.lock && ( -
      - - -
      - )} - - )} -
      - ) - ); -}); - -export default FileConfigs; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeConfigManipulations.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeConfigManipulations.tsx deleted file mode 100644 index 61cb5c877..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeConfigManipulations.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import { observer } from 'mobx-react'; -import { useTranslation } from 'react-i18next'; -import { Button } from 'hubble-ui'; - -interface TypeConfigManipulationsProps { - type: 'vertex' | 'edge'; - status: 'add' | 'edit'; - disableSave?: boolean; - onCreate: () => void; - onCancel: () => void; -} - -const TypeConfigManipulations: React.FC = observer( - ({ type, status, onCreate, onCancel, disableSave = false }) => { - const { t } = useTranslation(); - - return ( -
      - -
      - - -
      -
      - ); - } -); - -export default TypeConfigManipulations; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeConfigs.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeConfigs.tsx deleted file mode 100644 index f50a71637..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeConfigs.tsx +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useState, useEffect, useRef } from 'react'; -import { observer } from 'mobx-react'; -import { isEmpty, size } from 'lodash-es'; -import { useLocation } from 'wouter'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Button } from 'hubble-ui'; - -import { DataImportRootStoreContext } from '../../../../../stores'; - -import { Tooltip } from '../../../../common'; -import TypeInfo from './TypeInfo'; -import VertexMap from './VertexMap'; -import EdgeMap from './EdgeMap'; - -import ArrowIcon from '../../../../../assets/imgs/ic_arrow_16.svg'; - -const TypeConfigs: React.FC = observer(() => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const [isExpand, switchExpand] = useState(true); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [isCreateVertexMap, switchCreateVertexMap] = useState(false); - const [isCreateEdgeMap, switchCreateEdgeMap] = useState(false); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - const shouldRevealInitalButtons = - !isCreateVertexMap && - !isCreateEdgeMap && - isEmpty(dataMapStore.selectedFileInfo?.vertex_mappings) && - isEmpty(dataMapStore.selectedFileInfo?.edge_mappings); - - const invalidFileMaps = !dataMapStore.isIrregularProcess - ? dataMapStore.fileMapInfos.filter( - ({ name, vertex_mappings, edge_mappings }) => - dataImportRootStore.successFileUploadTaskNames.includes(name) && - isEmpty(vertex_mappings) && - isEmpty(edge_mappings) - ) - : dataMapStore.fileMapInfos.filter( - ({ vertex_mappings, edge_mappings }) => - isEmpty(vertex_mappings) && isEmpty(edge_mappings) - ); - - const expandClassName = classnames({ - 'import-tasks-step-content-header-expand': isExpand, - 'import-tasks-step-content-header-collpase': !isExpand - }); - - const nextButtonTooltipClassName = classnames({ - 'import-tasks-data-map-tooltip': true, - 'no-display': size(invalidFileMaps) === 0 - }); - - const handleExpand = () => { - switchExpand(!isExpand); - }; - - const handleCreate = (type: 'vertex' | 'edge', flag: boolean) => () => { - dataMapStore.switchExpand('file', false); - // Adding a new type config counts editing as well - dataMapStore.switchAddNewTypeConfig(flag); - - if (type === 'vertex') { - switchCreateVertexMap(flag); - } else { - switchCreateEdgeMap(flag); - } - }; - - useEffect(() => { - // close dashboard when user selects another file - // since the state is not in mobx store, we have to do this for now - switchCreateVertexMap(false); - switchCreateEdgeMap(false); - switchExpand(true); - }, [dataMapStore.selectedFileId]); - - return ( -
      -
      - - {t('data-configs.type.title')} - - collpaseOrExpand - {!dataMapStore.readOnly && - !dataMapStore.lock && - !shouldRevealInitalButtons && ( - - )} -
      - {!dataMapStore.readOnly && - !dataMapStore.lock && - shouldRevealInitalButtons && ( - - )} - {isExpand && ( - <> - {isCreateVertexMap && ( - - )} - {isCreateEdgeMap && ( - - )} - {dataMapStore.selectedFileInfo?.vertex_mappings - .map((mapping, index) => ( - - )) - .reverse()} - {dataMapStore.selectedFileInfo?.edge_mappings - .map((mapping, index) => ( - - )) - .reverse()} - - )} - {!dataMapStore.readOnly && ( -
      - {!serverDataImportStore.isServerStartImport && ( - - )} - -
      - {t('data-configs.type.hint.no-vertex-or-edge-mapping')} -
      - {invalidFileMaps.map(({ name }) => ( -
      - {name} -
      - ))} - - } - > - -
      -
      - )} -
      - ); -}); - -export interface TypeConfigMapCreationsProps { - onCreateVertex: () => void; - onCreateEdge: () => void; - disabled?: boolean; -} - -const TypeConfigMapCreations: React.FC = observer( - ({ onCreateVertex, onCreateEdge, disabled = false }) => { - const { t } = useTranslation(); - - return ( -
      - - -
      - ); - } -); - -export default TypeConfigs; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeInfo.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeInfo.tsx deleted file mode 100644 index 2274f54e6..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/TypeInfo.tsx +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useState, - useRef, - useContext, - useCallback, - useEffect -} from 'react'; -import { observer } from 'mobx-react'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Button } from 'hubble-ui'; - -import { Tooltip } from '../../../../common'; -import { DataImportRootStoreContext } from '../../../../../stores'; -import VertexMap from './VertexMap'; -import EdgeMap from './EdgeMap'; - -import ArrowIcon from '../../../../../assets/imgs/ic_arrow_16.svg'; -import { VertexType } from '../../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -export interface TypeInfoProps { - type: 'vertex' | 'edge'; - mapIndex: number; -} - -const TypeInfo: React.FC = observer(({ type, mapIndex }) => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [isExpand, switchExpand] = useState(false); - const [isDeletePop, switchDeletePop] = useState(false); - const [checkOrEdit, setCheckOrEdit] = useState<'check' | 'edit' | boolean>( - false - ); - const typeInfoWrapperRef = useRef(null); - const manipulationAreaRef = useRef(null); - const editButtonRef = useRef(null); - const deleteButtonRef = useRef(null); - const deleteWrapperRef = useRef(null); - const { t } = useTranslation(); - - let vertexInfo: VertexType | undefined; - let sourceVertexInfo: VertexType | undefined; - let targetVertexInfo: VertexType | undefined; - - if (type === 'vertex') { - vertexInfo = dataImportRootStore.vertexTypes.find( - ({ name }) => - name === dataMapStore.selectedFileInfo?.vertex_mappings[mapIndex].label - ); - } else { - const edgeInfo = dataImportRootStore.edgeTypes.find( - ({ name }) => - name === dataMapStore.selectedFileInfo!.edge_mappings[mapIndex].label - ); - - sourceVertexInfo = dataImportRootStore.vertexTypes.find( - ({ name }) => name === edgeInfo?.source_label - ); - - targetVertexInfo = dataImportRootStore.vertexTypes.find( - ({ name }) => name === edgeInfo?.target_label - ); - } - - const handleImgClickExpand = () => { - switchExpand(!isExpand); - - if (Boolean(checkOrEdit)) { - if (checkOrEdit === 'edit') { - dataMapStore.switchEditTypeConfig(false); - } - - setCheckOrEdit(false); - } else { - if (type === 'vertex') { - dataMapStore.syncEditMap('vertex', mapIndex); - } else { - dataMapStore.syncEditMap('edge', mapIndex); - } - - setCheckOrEdit('check'); - } - }; - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isDeletePop && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchDeletePop(false); - } - }, - [isDeletePop] - ); - - const handleTypeInfoExpandClick = useCallback( - (e: MouseEvent) => { - if ( - manipulationAreaRef.current && - !manipulationAreaRef.current?.contains(e.target as Element) - ) { - handleImgClickExpand(); - } - }, - [handleImgClickExpand] - ); - - const expandClassName = classnames({ - expand: isExpand, - collpase: !isExpand - }); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - typeInfoWrapperRef.current!.addEventListener( - 'click', - handleTypeInfoExpandClick, - false - ); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - typeInfoWrapperRef.current!.removeEventListener( - 'click', - handleTypeInfoExpandClick, - false - ); - }; - }, [handleOutSideClick, handleTypeInfoExpandClick]); - - return ( -
      -
      -
      -
      - collpaseOrExpand -
      -
      - - {t('data-configs.type.info.type')} - - - {type === 'vertex' - ? t('data-configs.type.vertex.type') - : t('data-configs.type.edge.type')} - -
      -
      -
      - - {t('data-configs.type.info.name')} - - - {type === 'vertex' - ? dataMapStore.selectedFileInfo?.vertex_mappings[mapIndex].label - : dataMapStore.selectedFileInfo?.edge_mappings[mapIndex].label} - -
      -
      - - {t('data-configs.type.info.ID-strategy')} - - - {type === 'vertex' - ? t(`data-configs.type.ID-strategy.${vertexInfo?.id_strategy}`) + - (vertexInfo?.id_strategy === 'PRIMARY_KEY' - ? '-' + vertexInfo?.primary_keys.join(',') - : '') - : `${t('addition.common.source')}:${ - t( - `data-configs.type.ID-strategy.${sourceVertexInfo?.id_strategy}` - ) + - (sourceVertexInfo?.id_strategy === 'PRIMARY_KEY' - ? '-' + sourceVertexInfo?.primary_keys.join(',') - : '') - } ${t('addition.common.target')}:${ - t( - `data-configs.type.ID-strategy.${targetVertexInfo?.id_strategy}` - ) + - (targetVertexInfo?.id_strategy === 'PRIMARY_KEY' - ? '-' + targetVertexInfo?.primary_keys.join(',') - : '') - }`} - -
      -
      - {!dataMapStore.readOnly && !dataMapStore.lock && ( - <> - - -

      - {t('data-configs.manipulations.hints.delete-confirm')} -

      -

      {t('data-configs.manipulations.hints.warning')}

      -
      - - -
      -
      - } - childrenProps={{ - onClick() { - switchDeletePop(true); - } - }} - > - - - - )} -
      -
      - {isExpand && Boolean(checkOrEdit) && type === 'vertex' && ( - { - setCheckOrEdit(false); - switchExpand(false); - }} - vertexMapIndex={mapIndex} - /> - )} - {isExpand && Boolean(checkOrEdit) && type === 'edge' && ( - { - setCheckOrEdit(false); - switchExpand(false); - }} - edgeMapIndex={mapIndex} - /> - )} - - ); -}); - -export default TypeInfo; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/VertexMap.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/VertexMap.tsx deleted file mode 100644 index b9e2f283e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/VertexMap.tsx +++ /dev/null @@ -1,1628 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useCallback, - useRef, - useEffect -} from 'react'; -import { observer } from 'mobx-react'; -import { isUndefined, isEmpty, size, cloneDeep } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Input, Select, Checkbox, Message } from 'hubble-ui'; - -import { Tooltip } from '../../../../common'; -import TypeConfigManipulations from './TypeConfigManipulations'; -import { DataImportRootStoreContext } from '../../../../../stores'; -import { VertexType } from '../../../../../stores/types/GraphManagementStore/metadataConfigsStore'; -import { getUnicodeLength } from '../../../../../utils'; - -import ArrowIcon from '../../../../../assets/imgs/ic_arrow_16.svg'; -import BlueArrowIcon from '../../../../../assets/imgs/ic_arrow_blue.svg'; -import CloseIcon from '../../../../../assets/imgs/ic_close_16.svg'; -import MapIcon from '../../../../../assets/imgs/ic_yingshe_16.svg'; - -export interface VertexMapProps { - checkOrEdit: 'check' | 'edit' | boolean; - onCancelCreateVertex: () => void; - vertexMapIndex?: number; -} - -const VertexMap: React.FC = observer( - ({ checkOrEdit, onCancelCreateVertex, vertexMapIndex }) => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore } = dataImportRootStore; - const [isAddMapping, switchAddMapping] = useState(false); - const [isExpandAdvance, switchExpandAdvance] = useState(false); - const addMappingWrapperRef = useRef(null); - const { t } = useTranslation(); - - const isCheck = checkOrEdit === 'check'; - const isEdit = checkOrEdit === 'edit'; - const vertexMap = - checkOrEdit !== false - ? dataMapStore.editedVertexMap! - : dataMapStore.newVertexType; - const filteredColumnNamesInSelection = - checkOrEdit !== false - ? dataMapStore.filteredColumnNamesInVertexEditSelection - : dataMapStore.filteredColumnNamesInVertexNewSelection; - - const findVertex = (collection: VertexType[], label: string) => - collection.find(({ name }) => name === label); - - const selectedVertex = findVertex( - dataImportRootStore.vertexTypes, - vertexMap.label - ); - - const isStrategyAutomatic = selectedVertex?.id_strategy === 'AUTOMATIC'; - - const handleAdvanceExpand = () => { - switchExpandAdvance(!isExpandAdvance); - }; - - // need useCallback to stop infinite callings of useEffect - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - // if clicked element is not on dropdown, collpase it - if ( - isAddMapping && - addMappingWrapperRef.current && - !addMappingWrapperRef.current.contains(e.target as Element) - ) { - switchAddMapping(false); - } - }, - [isAddMapping] - ); - - const wrapperName = classnames({ - 'import-tasks-data-map-config-card': !Boolean(checkOrEdit), - 'import-tasks-data-map-config-view': Boolean(checkOrEdit) - }); - - const expandAdvanceClassName = classnames({ - 'import-tasks-step-content-header-expand': isExpandAdvance, - 'import-tasks-step-content-header-collpase': !isExpandAdvance - }); - - const expandAddMapClassName = classnames({ - 'import-tasks-step-content-header-expand': isAddMapping, - 'import-tasks-step-content-header-collpase': !isAddMapping - }); - - const addMappingManipulationClassName = classnames({ - 'import-tasks-manipulation': true, - 'import-tasks-manipulation-disabled': - size(filteredColumnNamesInSelection) === 0 || isStrategyAutomatic - }); - - const addNullValueClassName = classnames({ - 'import-tasks-manipulation': true, - 'import-tasks-manipulation-disabled': isEdit - ? dataMapStore.editedVertexMap!.null_values.customized.includes('') - : dataMapStore.newVertexType.null_values.customized.includes('') - }); - - const addPropertyMapClassName = classnames({ - 'import-tasks-manipulation': true, - 'import-tasks-manipulation-disabled': - !dataMapStore.allowAddPropertyMapping('vertex') || isStrategyAutomatic - }); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( -
      - {Boolean(checkOrEdit) ? ( -
      - {t('data-configs.type.basic-settings')} -
      - ) : ( -
      - {t('data-configs.type.vertex.title')} - collpaseOrExpand -
      - )} -
      - - {t('data-configs.type.vertex.type')}: - - {isCheck ? ( - {dataMapStore.editedVertexMap!.label} - ) : ( - - )} -
      - {!isUndefined(selectedVertex) && ( -
      - - {t('data-configs.type.vertex.ID-strategy')}: - - - {t('data-configs.type.hint.lack-support-for-automatic')} - - } - > - {t(`data-configs.type.ID-strategy.${selectedVertex.id_strategy}`)} - {selectedVertex.id_strategy === 'PRIMARY_KEY' && - `-${selectedVertex.primary_keys.join(',')}`} - -
      - )} - {vertexMap.id_fields.map((idField, fieldIndex) => { - return ( -
      - - {t('data-configs.type.vertex.ID-column') + - (selectedVertex?.id_strategy === 'PRIMARY_KEY' - ? fieldIndex + 1 - : '')} - : - - {isCheck ? ( - {idField} - ) : ( - - )} -
      - ); - })} - -
      - - {t('data-configs.type.vertex.map-settings')}: - -
      - {isCheck && - isEmpty(dataMapStore.editedVertexMap?.field_mapping) && - '-'} - - {((Boolean(checkOrEdit) === false && - !isEmpty(dataMapStore.newVertexType.field_mapping)) || - (Boolean(checkOrEdit) !== false && - !isEmpty(dataMapStore.editedVertexMap!.field_mapping))) && ( -
      -
      - {t('data-configs.type.vertex.add-map.name')} -
      -
      - {t('data-configs.type.vertex.add-map.sample')} -
      -
      - {t('data-configs.type.vertex.add-map.property')} -
      -
      - {!isCheck && ( - { - isEdit - ? dataMapStore.toggleVertexSelectAllFieldMapping( - 'edit', - false, - selectedVertex - ) - : dataMapStore.toggleVertexSelectAllFieldMapping( - 'new', - false, - selectedVertex - ); - }} - > - {t('data-configs.type.vertex.add-map.clear')} - - )} -
      -
      - )} - - {vertexMap.field_mapping.map( - ({ column_name, mapped_name }, fieldIndex) => { - const param = checkOrEdit === false ? 'new' : 'edit'; - - return ( -
      -
      - {column_name} -
      -
      - { - dataMapStore.selectedFileInfo?.file_setting - .column_values[ - dataMapStore.selectedFileInfo?.file_setting.column_names.findIndex( - (name) => column_name === name - ) - ] - } -
      -
      - {isCheck ? ( - {mapped_name} - ) : ( - - )} -
      -
      - {!isCheck && ( - close { - dataMapStore.removeVertexFieldMapping( - param, - column_name - ); - }} - /> - )} -
      -
      - ); - } - )} - - {!isCheck && ( - <> -
      { - if ( - size(filteredColumnNamesInSelection) === 0 || - isStrategyAutomatic - ) { - switchAddMapping(false); - return; - } - - switchAddMapping(!isAddMapping); - }} - > -
      - {t('data-configs.type.vertex.add-map.title')} -
      - expand -
      - {isAddMapping && ( -
      -
      - - { - if (isEdit) { - const isIndeterminate = - !isEmpty( - dataMapStore.editedVertexMap?.field_mapping - ) && - size( - dataMapStore.editedVertexMap?.field_mapping - ) !== - size( - dataMapStore.filteredColumnNamesInVertexEditSelection - ); - - dataMapStore.toggleVertexSelectAllFieldMapping( - 'edit', - // if isIndeterminate is true, e.target.checked is false - isIndeterminate || e.target.checked, - selectedVertex - ); - } else { - const isIndeterminate = - !isEmpty( - dataMapStore.newVertexType.field_mapping - ) && - size( - dataMapStore.newVertexType.field_mapping - ) !== - size( - dataMapStore.filteredColumnNamesInVertexNewSelection - ); - - dataMapStore.toggleVertexSelectAllFieldMapping( - 'new', - isIndeterminate || e.target.checked, - selectedVertex - ); - } - }} - > - {t('data-configs.type.vertex.select-all')} - - -
      - {dataMapStore.selectedFileInfo?.file_setting.column_names - .filter((name) => !vertexMap.id_fields.includes(name)) - .map((name) => { - let combinedText = name; - let currentColumnValue = dataMapStore.selectedFileInfo - ?.file_setting.column_values[ - dataMapStore.selectedFileInfo?.file_setting.column_names.findIndex( - (columnName) => name === columnName - ) - ] as string; - combinedText += `(${currentColumnValue})`; - - if (getUnicodeLength(combinedText) > 35) { - combinedText = combinedText.slice(0, 35) + '...'; - } - - const mappingValue = selectedVertex?.properties.find( - ({ name: propertyName }) => propertyName === name - )?.name; - - return ( -
      - - column_name === name - ) - ) - } - onChange={(e: any) => { - if (e.target.checked) { - isEdit - ? dataMapStore.setVertexFieldMappingKey( - 'edit', - name, - mappingValue - ) - : dataMapStore.setVertexFieldMappingKey( - 'new', - name, - mappingValue - ); - } else { - isEdit - ? dataMapStore.removeVertexFieldMapping( - 'edit', - name - ) - : dataMapStore.removeVertexFieldMapping( - 'new', - name - ); - } - }} - > - {combinedText} - - -
      - ); - })} -
      - )} - - )} -
      -
      - -
      - {t('data-configs.type.vertex.advance.title')} - {!isCheck && ( - collpaseOrExpand - )} -
      - {(isExpandAdvance || isCheck) && ( - <> -
      - - {t('data-configs.type.vertex.advance.nullable-list.title')}: - - {isCheck ? ( - - {dataMapStore - .editedVertexMap!.null_values.checked.filter( - (value) => value !== 'null' - ) - .map((value) => - value === '' - ? t('addition.common.null-value') - : value === 'NULL' - ? 'NULL/null' - : value - ) - .concat( - dataMapStore.editedVertexMap!.null_values.customized - ) - .join(',')} - - ) : ( - <> -
      - { - isEdit - ? dataMapStore.editCheckedNullValues( - 'edit', - 'vertex', - checkedList - ) - : dataMapStore.editCheckedNullValues( - 'new', - 'vertex', - checkedList - ); - }} - value={ - isEdit - ? dataMapStore.editedVertexMap!.null_values.checked - : dataMapStore.newVertexType.null_values.checked - } - > - NULL/null - - {t( - 'data-configs.type.vertex.advance.nullable-list.empty' - )} - - -
      - { - dataMapStore.toggleCustomNullValue( - isEdit ? 'edit' : 'new', - 'vertex', - e.target.checked - ); - - if (e.target.checked) { - dataMapStore.addValidateValueMappingOption( - 'null_values' - ); - } else { - dataMapStore.resetValidateValueMapping( - 'null_values' - ); - } - }} - > - {t( - 'data-configs.type.vertex.advance.nullable-list.custom' - )} - -
      -
      -
      - {isEdit ? ( - <> - {dataMapStore.editedVertexMap?.null_values.customized.map( - (nullValue, nullValueIndex) => ( -
      - { - dataMapStore.editCustomNullValues( - 'edit', - 'vertex', - e.value, - nullValueIndex - ); - - dataMapStore.validateValueMapping( - 'vertex', - 'edit', - 'null_values', - nullValueIndex - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore.validateAdvanceConfigErrorMessage - .null_values[nullValueIndex] - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'vertex', - 'edit', - 'null_values', - nullValueIndex - ); - } - }} - /> -
      - ) - )} - {!isEmpty( - dataMapStore.editedVertexMap?.null_values.customized - ) && ( -
      - { - const extraNullValues = - dataMapStore.editedVertexMap?.null_values - .customized; - - if (!extraNullValues?.includes('')) { - dataMapStore.addCustomNullValues( - 'edit', - 'vertex' - ); - } - }} - > - {t('data-configs.manipulations.add')} - -
      - )} - - ) : ( - <> - {dataMapStore.newVertexType.null_values.customized.map( - (nullValue, nullValueIndex) => ( -
      - { - dataMapStore.editCustomNullValues( - 'new', - 'vertex', - e.value, - nullValueIndex - ); - - dataMapStore.validateValueMapping( - 'vertex', - 'new', - 'null_values', - nullValueIndex - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore.validateAdvanceConfigErrorMessage - .null_values[nullValueIndex] - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'vertex', - 'new', - 'null_values', - nullValueIndex - ); - } - }} - /> -
      - ) - )} - {!isEmpty( - dataMapStore.newVertexType.null_values.customized - ) && ( -
      - { - const extraNullValues = - dataMapStore.newVertexType?.null_values - .customized; - - if (!extraNullValues.includes('')) { - dataMapStore.addCustomNullValues( - 'new', - 'vertex' - ); - } - }} - > - {t('data-configs.manipulations.add')} - -
      - )} - - )} -
      - - )} -
      - -
      - - {t('data-configs.type.vertex.advance.map-property-value.title')} - : - - {!isCheck && - (isEdit - ? isEmpty(dataMapStore.editedVertexMap?.value_mapping) - : isEmpty(dataMapStore.newVertexType.value_mapping)) && ( -
      { - if (isStrategyAutomatic) { - return; - } - - isEdit - ? dataMapStore.addVertexValueMapping('edit') - : dataMapStore.addVertexValueMapping('new'); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings' - ); - }} - > - {t( - 'data-configs.type.vertex.advance.map-property-value.add-value' - )} -
      - )} - - {isCheck && ( -
      - {isEmpty(dataMapStore.editedVertexMap?.value_mapping) - ? '-' - : dataMapStore.editedVertexMap?.value_mapping.map( - ({ column_name, values }, valueMapIndex) => ( -
      -
      - - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.property' - )} - {valueMapIndex + 1}: - - {column_name} -
      - {values.map(({ column_value, mapped_value }) => ( -
      - - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.value-map' - )} - - {column_value} - map - {mapped_value} -
      - ))} -
      - ) - )} -
      - )} -
      - - {/* property value mapping form */} - {!isCheck && - (!Boolean(checkOrEdit) - ? dataMapStore.newVertexType.value_mapping.map( - ({ column_name, values }, valueMapIndex) => ( -
      -
      - - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.property' - )} - {valueMapIndex + 1}: - - - { - dataMapStore.removeVertexValueMapping( - 'new', - valueMapIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'vertex', - 'new', - 'value_mappings', - valueMapIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - -
      -
      - - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.value-map' - )} - : - -
      - {values.map( - ({ column_value, mapped_value }, valueIndex) => ( -
      - { - dataMapStore.editVertexValueMappingColumnValueName( - 'new', - 'column_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'vertex', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].column_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'vertex', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - } - }} - /> - map - { - dataMapStore.editVertexValueMappingColumnValueName( - 'new', - 'mapped_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'vertex', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].mapped_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'vertex', - 'new', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - } - }} - /> - {values.length > 1 && ( - { - dataMapStore.removeVertexValueMappingValue( - 'new', - valueMapIndex, - valueIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'vertex', - 'new', - 'value_mappings_value', - valueMapIndex, - valueIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - - )} -
      - ) - )} -
      -
      - { - if ( - !dataMapStore.allowAddPropertyValueMapping( - 'vertex', - valueMapIndex - ) - ) { - return; - } - - dataMapStore.addVertexValueMappingValue( - 'new', - valueMapIndex - ); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings_value', - valueMapIndex - ); - }} - > - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.add-value-map' - )} - -
      -
      -
      -
      -
      - ) - ) - : dataMapStore.editedVertexMap?.value_mapping.map( - ({ column_name, values }, valueMapIndex) => ( -
      -
      - - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.property' - )} - {valueMapIndex + 1}: - - - { - dataMapStore.removeVertexValueMapping( - 'edit', - valueMapIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'vertex', - 'edit', - 'value_mappings', - valueMapIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - -
      -
      - - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.value-map' - )} - : - -
      - {values.map( - ({ column_value, mapped_value }, valueIndex) => ( -
      - { - dataMapStore.editVertexValueMappingColumnValueName( - 'edit', - 'column_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'vertex', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].column_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'vertex', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'column_value', - valueIndex: valueIndex - } - ); - } - }} - /> - map - { - dataMapStore.editVertexValueMappingColumnValueName( - 'edit', - 'mapped_value', - e.value, - valueMapIndex, - valueIndex - ); - - dataMapStore.validateValueMapping( - 'vertex', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - }} - errorLocation="layer" - errorMessage={ - dataMapStore - .validateAdvanceConfigErrorMessage - .value_mapping[valueMapIndex].values[ - valueIndex - ].mapped_value - } - originInputProps={{ - onBlur: () => { - dataMapStore.validateValueMapping( - 'vertex', - 'edit', - 'value_mappings', - valueMapIndex, - { - field: 'mapped_value', - valueIndex: valueIndex - } - ); - } - }} - /> - {values.length > 1 && ( - { - dataMapStore.removeVertexValueMappingValue( - 'edit', - valueMapIndex, - valueIndex - ); - - dataMapStore.removeValidateValueMappingOption( - 'vertex', - 'edit', - 'value_mappings_value', - valueMapIndex, - valueIndex - ); - }} - > - {t('data-configs.manipulations.delete')} - - )} -
      - ) - )} -
      -
      - { - if ( - !dataMapStore.allowAddPropertyValueMapping( - 'vertex', - valueMapIndex - ) - ) { - return; - } - - dataMapStore.addVertexValueMappingValue( - 'edit', - valueMapIndex - ); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings_value', - valueMapIndex - ); - }} - > - {t( - 'data-configs.type.vertex.advance.map-property-value.fields.add-value-map' - )} - -
      -
      -
      -
      -
      - ) - ))} - - {!isCheck && - (isEdit - ? !isEmpty(dataMapStore.editedVertexMap?.value_mapping) - : !isEmpty(dataMapStore.newVertexType.value_mapping)) && ( -
      { - if (!dataMapStore.allowAddPropertyMapping('vertex')) { - return; - } - - isEdit - ? dataMapStore.addVertexValueMapping('edit') - : dataMapStore.addVertexValueMapping('new'); - - dataMapStore.addValidateValueMappingOption( - 'value_mappings' - ); - }} - > - {t( - 'data-configs.type.vertex.advance.map-property-value.add-value' - )} -
      - )} - - )} - - {!isCheck && ( - - isEmpty(mapped_name) - ) || - !dataMapStore.allowAddPropertyMapping('vertex') || - !dataMapStore.isValidateSave - } - onCreate={async () => { - dataMapStore.switchAddNewTypeConfig(false); - dataMapStore.switchEditTypeConfig(false); - - // really weird! if import task comes from import-manager - // datamapStore.fileMapInfos cannot be updated in - // though it's already re-rendered - if (dataMapStore.isIrregularProcess) { - isEdit - ? await dataMapStore.updateVertexMap( - 'upgrade', - dataMapStore.selectedFileId - ) - : await dataMapStore.updateVertexMap( - 'add', - dataMapStore.selectedFileId - ); - - dataMapStore.fetchDataMaps(); - } else { - isEdit - ? await dataMapStore.updateVertexMap( - 'upgrade', - dataMapStore.selectedFileId - ) - : await dataMapStore.updateVertexMap( - 'add', - dataMapStore.selectedFileId - ); - } - - if (dataMapStore.requestStatus.updateVertexMap === 'failed') { - Message.error({ - content: dataMapStore.errorInfo.updateVertexMap.message, - size: 'medium', - showCloseIcon: false - }); - } - - onCancelCreateVertex(); - dataMapStore.resetNewMap('vertex'); - }} - onCancel={() => { - dataMapStore.switchAddNewTypeConfig(false); - dataMapStore.switchEditTypeConfig(false); - - if (!isEdit) { - dataMapStore.resetNewMap('vertex'); - } else { - dataMapStore.resetEditMapping('vertex'); - } - - onCancelCreateVertex(); - dataMapStore.resetNewMap('vertex'); - dataMapStore.resetValidateValueMapping('all'); - }} - /> - )} -
      - ); - } -); - -export default VertexMap; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/index.ts deleted file mode 100644 index 44d62e9c2..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/datamap-configs/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import DataMapConfigs from './DataMapConfigs'; - -export { DataMapConfigs }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/JobErrorLogs.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/JobErrorLogs.less deleted file mode 100644 index ca243f647..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/JobErrorLogs.less +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.job-error-logs { - width: 75vw; - position: relative; - top: 76px; - margin: 0 auto; - font-size: 14px; - - &-title { - margin-bottom: 16px; - color: #333; - line-height: 22px; - font-weight: 900; - } - - &-content-wrapper { - display: flex; - height: calc(100vh - 139px); - padding-top: 25px; - padding-right: 16px; - background-color: #fff; - } - - &-content { - margin-left: 19px; - overflow: auto; - line-height: 2; - - &-item { - display: flex; - - &:nth-child(even) { - margin-bottom: 32px; - } - } - - &-item-title { - word-break: keep-all; - } - - &-item-text { - margin-left: 6px; - } - - &-with-error-only { - margin-left: 16px; - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/JobErrorLogs.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/JobErrorLogs.tsx deleted file mode 100644 index 0cb48f63c..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/JobErrorLogs.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect, useState } from 'react'; -import { useRoute } from 'wouter'; -import { observer } from 'mobx-react'; -import { motion } from 'framer-motion'; -import { Menu } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { ImportManagerStoreContext } from '../../../../../stores'; - -import './JobErrorLogs.less'; - -const failedReasonVariants = { - initial: { - opacity: 0, - y: -10 - }, - animate: { - opacity: 1, - y: 0, - transition: { - duration: 0.8, - ease: 'easeInOut' - } - } -}; - -const JobErrorLogs: React.FC = observer(() => { - const importManagerStore = useContext(ImportManagerStoreContext); - const { t } = useTranslation(); - const [, params] = useRoute( - '/graph-management/:id/data-import/job-error-log/:jobId' - ); - const [selectedFileName, setSelectedFileName] = useState(''); - - useEffect(() => { - const init = async () => { - await importManagerStore.fetchFailedReason( - Number(params!.id), - Number(params!.jobId) - ); - - if (importManagerStore.requestStatus.fetchFailedReason === 'success') { - setSelectedFileName(importManagerStore.failedReason[0].file_name); - } - }; - - init(); - }, [params!.id, params!.jobId]); - - return ( -
      -
      - {t('addition.message.fail-reason')} -
      - -
      - {importManagerStore.requestStatus.fetchFailedReason === 'failed' ? ( -
      - {importManagerStore.errorInfo.fetchFailedReason.message} -
      - ) : ( - <> - { - setSelectedFileName(e.key); - }} - > - {importManagerStore.failedReason.map(({ file_name }) => ( - - {file_name} - - ))} - -
      - {importManagerStore.failedReason - .find(({ file_name }) => file_name === selectedFileName) - ?.reason.split('\n') - .filter((reason) => reason !== '') - .map((text, index) => ( -
      -
      - {index % 2 === 0 - ? `${t('addition.message.fail-reason')}:` - : `${t('addition.message.fail-position')}:`} -
      -
      - {text.replace('#### INSERT ERROR:', '')} -
      -
      - ))} -
      - - )} -
      -
      -
      - ); -}); - -export default JobErrorLogs; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/TaskErrorLogs.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/TaskErrorLogs.less deleted file mode 100644 index 7e992bfc7..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/TaskErrorLogs.less +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.task-error-logs { - width: 80vw; - position: relative; - top: 76px; - line-height: 2; - font-size: 14px; - margin: 0 auto 16px; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/TaskErrorLogs.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/TaskErrorLogs.tsx deleted file mode 100644 index d71097365..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/TaskErrorLogs.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useEffect } from 'react'; -import { useRoute } from 'wouter'; -import { observer } from 'mobx-react'; -import { DataImportRootStoreContext } from '../../../../../stores'; - -import './TaskErrorLogs.less'; - -const TaskErrorLogs: React.FC = observer(() => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { serverDataImportStore } = dataImportRootStore; - const [, params] = useRoute( - '/graph-management/:id/data-import/:jobId/task-error-log/:taskId' - ); - - useEffect(() => { - serverDataImportStore.checkErrorLogs( - Number(params!.id), - Number(params!.jobId), - Number(params!.taskId) - ); - }, [params!.id, params!.jobId, params!.taskId]); - - return ( -
      - {/*
      - {serverDataImportStore.requestStatus.checkErrorLogs === 'failed' - ? serverDataImportStore.errorInfo.checkErrorLogs.message - : serverDataImportStore.errorLogs} -
      */} -
      - {serverDataImportStore.requestStatus.checkErrorLogs === 'failed' - ? serverDataImportStore.errorInfo.checkErrorLogs.message - : serverDataImportStore.errorLogs - .split('\n') - .map((text) =>

      {text}

      )} -
      -
      - ); -}); - -export default TaskErrorLogs; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/index.ts deleted file mode 100644 index a694b2fc5..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/error-logs/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import TaskErrorLogs from './TaskErrorLogs'; -import JobErrorLogs from './JobErrorLogs'; - -export { TaskErrorLogs, JobErrorLogs }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/index.ts deleted file mode 100644 index e1324524f..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import ImportTasks from './ImportTasks'; -import ImportManager from './ImportManager'; -import { JobDetails } from './job-details'; - -export { ImportTasks, ImportManager, JobDetails }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/BasicSettings.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/BasicSettings.tsx deleted file mode 100644 index 0332c4b83..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/BasicSettings.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useContext, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { isEmpty, isNull, isUndefined } from 'lodash-es'; -import { useRoute } from 'wouter'; -import { motion } from 'framer-motion'; -import { useTranslation } from 'react-i18next'; -import { Button, Modal, Input, Message } from 'hubble-ui'; - -import { ImportManagerStoreContext } from '../../../../../stores'; -import { - GraphManagementStoreContext, - DataImportRootStoreContext -} from '../../../../../stores'; - -const BasicSettings: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const importManagerStore = useContext(ImportManagerStoreContext); - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { dataMapStore, serverDataImportStore } = dataImportRootStore; - const [isPopEditModal, switchPopEditModal] = useState(false); - const [, params] = useRoute( - '/graph-management/:id/data-import/import-manager/:jobId/details' - ); - const { t } = useTranslation(); - - useEffect(() => { - const init = async () => { - if (isEmpty(importManagerStore.importJobList)) { - graphManagementStore.fetchIdList(); - importManagerStore.setCurrentId(Number(params!.id)); - await importManagerStore.fetchImportJobList(); - } - - if (isNull(importManagerStore.selectedJob)) { - if ( - !isUndefined( - importManagerStore.importJobList.find( - ({ id }) => String(id) === (params && params.jobId) - ) - ) && - params !== null - ) { - importManagerStore.setSelectedJob(Number(params.jobId)); - - // duplicate logic in - // fill in essential data in import-task stores - dataImportRootStore.setCurrentId(Number(params!.id)); - dataImportRootStore.setCurrentJobId(Number(params.jobId)); - - dataImportRootStore.fetchVertexTypeList(); - dataImportRootStore.fetchEdgeTypeList(); - - // fetch related data - await Promise.all([ - dataMapStore.fetchDataMaps(), - serverDataImportStore.fetchAllImportTasks() - ]); - - dataMapStore.setSelectedFileId( - Number(dataMapStore.fileMapInfos[0].id) - ); - dataMapStore.setSelectedFileInfo(); - - // set flags about readonly and irregular process in - dataMapStore.switchReadOnly(true); - dataMapStore.switchIrregularProcess(true); - - // set flags about readonly and irregular process in - serverDataImportStore.switchExpandImportConfig(true); - serverDataImportStore.switchReadOnly(true); - serverDataImportStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - } - } - }; - - init(); - }, []); - - return ( -
      -
      -
      - {t('import-job-details.basic.job-name')} - {importManagerStore.selectedJob?.job_name} -
      -
      - {t('import-job-details.basic.job-description')} - {importManagerStore.selectedJob?.job_remarks} -
      -
      - - { - switchPopEditModal(false); - await importManagerStore.updateJobInfo(); - - if (importManagerStore.requestStatus.updateJobInfo === 'failed') { - Message.error({ - content: importManagerStore.errorInfo.updateJobInfo.message, - size: 'medium', - showCloseIcon: false - }); - - return; - } - - if ( - importManagerStore.requestStatus.updateJobInfo === 'success' - ) { - Message.success({ - content: t('import-manager.hint.update-succeed'), - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('import-job-details.basic.modal.manipulations.save')} - , - - ]} - destroyOnClose - needCloseIcon - onCancel={() => { - switchPopEditModal(false); - }} - > -
      -
      -
      - - * - - - {t('import-job-details.basic.modal.edit-job.job-name')} - -
      - { - importManagerStore.mutateEditJob('name', e.value); - importManagerStore.validateJob('edit', 'name'); - }} - originInputProps={{ - onBlur: () => { - importManagerStore.validateJob('edit', 'name'); - } - }} - /> -
      -
      -
      - {t('import-job-details.basic.modal.edit-job.job-description')} -
      -
      - { - importManagerStore.mutateEditJob('description', e.value); - importManagerStore.validateJob('edit', 'description'); - }} - originInputProps={{ - onBlur: () => { - importManagerStore.validateJob('edit', 'description'); - } - }} - /> -
      -
      -
      -
      -
      - ); -}); - -export default BasicSettings; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/DataImportDetails.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/DataImportDetails.tsx deleted file mode 100644 index 21b81eba0..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/DataImportDetails.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import { observer } from 'mobx-react'; - -import { ServerDataImport } from '../server-data-import'; - -const DataImportDetails: React.FC = observer(() => { - return ( -
      - -
      - ); -}); - -export default DataImportDetails; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/DataMaps.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/DataMaps.tsx deleted file mode 100644 index 1f205c6ae..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/DataMaps.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import { observer } from 'mobx-react'; - -import DataMapConfigs from '../datamap-configs/DataMapConfigs'; - -import '../datamap-configs/DataMapConfigs.less'; - -const DataMaps: React.FC = observer(() => { - return ( -
      - -
      - ); -}); - -export default DataMaps; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/JobDetails.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/JobDetails.less deleted file mode 100644 index 21bb217ba..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/JobDetails.less +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.import-job-details { - position: absolute; - width: calc(100% - 60px); - padding: 0 16px 16px; - left: 60px; - top: 60px; - - &-with-expand-sidebar { - width: calc(100% - 200px); - left: 200px; - } - - &-breadcrumb-wrapper { - display: flex; - justify-content: space-between; - align-items: center; - margin: 16px 0; - } - - &-content-wrapper { - height: calc(100vh - 201px); - background: #fff; - padding: 24px; - overflow: auto; - } - - &-content-header { - margin-bottom: 16px; - display: flex; - justify-content: flex-end; - } - - &-basic-text { - font-size: 14px; - color: #333; - - & > div { - margin-bottom: 14px; - } - - & span:last-child { - margin-left: 24px; - } - } - - &-uploaded-file-infos { - margin-bottom: 19px; - font-size: 14px; - - &-titles { - width: 300px; - display: flex; - justify-content: space-between; - } - - &-progress-status { - width: 374px; - display: flex; - align-items: center; - } - } -} - -/* override */ - -// reveal error layer in -.new-fc-one-modal-body { - overflow: visible; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/JobDetails.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/JobDetails.tsx deleted file mode 100644 index 66d7394ff..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/JobDetails.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { AnimatePresence } from 'framer-motion'; -import { useTranslation } from 'react-i18next'; -import { isEmpty } from 'lodash-es'; -import { Menu } from 'hubble-ui'; - -import { - DataImportRootStoreContext, - ImportManagerStoreContext -} from '../../../../../stores'; - -import BasicSettings from './BasicSettings'; -import UploadedFiles from './UploadedFiles'; -import DataMaps from './DataMaps'; -import DataImportDetails from './DataImportDetails'; - -import './JobDetails.less'; - -const JobDetails: React.FC = observer(() => { - const importManagerStore = useContext(ImportManagerStoreContext); - const { dataMapStore, serverDataImportStore } = useContext( - DataImportRootStoreContext - ); - const { t } = useTranslation(); - - const handleMenuItemChange = ({ key }: { key: string }) => { - importManagerStore.setCurrentJobDetailStep(key); - }; - - const renderListView = () => { - switch (importManagerStore.jobDetailsStep) { - case 'basic': - return ; - case 'upload': - return ; - case 'data-map': - return ; - case 'import-details': - return ; - } - }; - - return ( - <> - - - {t('import-job-details.tabs.basic-settings')} - - - {t('import-job-details.tabs.uploaded-files')} - - - {t('import-job-details.tabs.data-maps')} - - - {t('import-job-details.tabs.import-details')} - - - {renderListView()} - - ); -}); - -export default JobDetails; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/UploadedFiles.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/UploadedFiles.tsx deleted file mode 100644 index 7bcf4e6fc..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/UploadedFiles.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Progress } from 'hubble-ui'; - -import { DataImportRootStoreContext } from '../../../../../stores'; - -const UploadedFiles: React.FC = observer(() => { - const { dataMapStore } = useContext(DataImportRootStoreContext); - - return ( -
      - {dataMapStore.fileMapInfos.map(({ name, total_size }) => ( -
      -
      - {name} - {total_size} -
      -
      - -
      -
      - ))} -
      - ); -}); - -export default UploadedFiles; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/index.ts deleted file mode 100644 index 36247d378..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/job-details/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import JobDetails from './JobDetails'; - -export { JobDetails }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ImportConfigs.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ImportConfigs.tsx deleted file mode 100644 index 289e9d394..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ImportConfigs.tsx +++ /dev/null @@ -1,862 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useCallback, useEffect, useRef } from 'react'; -import { observer } from 'mobx-react'; -import { useLocation } from 'wouter'; -import { isEmpty } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; -import { Switch, Input, Button, Table, Tooltip, Message } from 'hubble-ui'; - -import { DataImportRootStoreContext } from '../../../../../stores'; -import { useInitDataImport } from '../../../../../hooks'; - -import type { - ImportTasks, - LoadParameter -} from '../../../../../stores/types/GraphManagementStore/dataImportStore'; - -import ArrowIcon from '../../../../../assets/imgs/ic_arrow_16.svg'; -import HintIcon from '../../../../../assets/imgs/ic_question_mark.svg'; - -const importStatusColorMapping: Record = { - RUNNING: '#2b65ff', - SUCCEED: '#39bf45', - FAILED: '#e64552', - PAUSED: '#a9cbfb', - STOPPED: '#ccc' -}; - -const commonInputProps = { - size: 'medium', - width: 100, - errorLocation: 'layer' -}; - -export interface ImportConfigsProps { - height?: string; -} - -const ImportConfigs: React.FC = observer(({ height }) => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { serverDataImportStore, dataMapStore } = dataImportRootStore; - const timerId = useRef(NaN); - const isInitReady = useInitDataImport(); - const [, setLocation] = useLocation(); - const { t } = useTranslation(); - - const columnConfigs = [ - { - title: t('server-data-import.import-details.column-titles.file-name'), - dataIndex: 'file_name', - width: '10%', - render(text: string) { - return ( -
      - {text} -
      - ); - } - }, - { - title: t('server-data-import.import-details.column-titles.type'), - dataIndex: 'type', - width: '15%', - render(_: any, rowData: ImportTasks) { - const title = ( -
      - {!isEmpty(rowData.vertices) && ( -
      - {t('server-data-import.import-details.content.vertex')}: - {rowData.vertices.join(' ')} -
      - )} - {!isEmpty(rowData.edges) && ( -
      - {t('server-data-import.import-details.content.edge')}: - {rowData.edges.join(' ')} -
      - )} - {isEmpty(rowData.vertices) && isEmpty(rowData.edges) && '-'} -
      - ); - - return ( - -
      - {!isEmpty(rowData.vertices) && - `${t( - 'server-data-import.import-details.content.vertex' - )}:${rowData.vertices.join(' ')} `} - {!isEmpty(rowData.edges) && - `${t( - 'server-data-import.import-details.content.edge' - )}:${rowData.edges.join(' ')}`} - {isEmpty(rowData.vertices) && isEmpty(rowData.edges) && '-'} -
      -
      - ); - } - }, - { - title: t('server-data-import.import-details.column-titles.import-speed'), - dataIndex: 'load_rate', - width: '12%', - align: 'center', - render(text: string) { - return ( -
      - {text} -
      - ); - } - }, - { - title: t( - 'server-data-import.import-details.column-titles.import-progress' - ), - dataIndex: 'load_progress', - width: '35%', - render(progress: number, rowData: Record) { - return ( -
      -
      -
      -
      -
      {`${progress}%`}
      -
      - ); - } - }, - { - title: t('server-data-import.import-details.column-titles.status'), - dataIndex: 'status', - width: '10%', - render(text: string) { - return ( -
      - {t(`server-data-import.import-details.status.${text}`)} -
      - ); - } - }, - { - title: t('server-data-import.import-details.column-titles.time-consumed'), - dataIndex: 'duration', - align: 'right', - width: '8%', - render(text: string) { - return ( -
      - {text} -
      - ); - } - }, - { - title: !serverDataImportStore.readOnly - ? t('server-data-import.import-details.column-titles.manipulations') - : '', - width: '15%', - render(_: never, rowData: Record, taskIndex: number) { - return !serverDataImportStore.readOnly ? ( -
      - -
      - ) : null; - } - } - ]; - - const handleInputChange = (key: keyof LoadParameter) => (e: any) => { - serverDataImportStore.mutateImportConfigs(key, e.value); - serverDataImportStore.validateImportConfigs(key); - }; - - const handleBlur = (key: keyof LoadParameter) => () => { - serverDataImportStore.validateImportConfigs(key); - }; - - const loopQueryImportData = useCallback(() => { - const loopId = window.setInterval(async () => { - if (serverDataImportStore.isImportFinished) { - window.clearInterval(loopId); - return; - } - - if ( - serverDataImportStore.requestStatus.fetchImportTasks === 'failed' || - serverDataImportStore.requestStatus.fetchAllImportTasks === 'failed' - ) { - Message.error({ - content: - serverDataImportStore.errorInfo.fetchImportTasks.message || - serverDataImportStore.errorInfo.fetchAllImportTasks.message, - size: 'medium', - showCloseIcon: false - }); - - window.clearInterval(loopId); - return; - } - - if (serverDataImportStore.isIrregularProcess) { - serverDataImportStore.fetchAllImportTasks(); - } else { - // stop loops when users click sidebar icon - // (dispose called in , [ids] resets to empty array) - if (isEmpty(serverDataImportStore.fileImportTaskIds)) { - window.clearInterval(loopId); - return; - } - - serverDataImportStore.fetchImportTasks( - serverDataImportStore.fileImportTaskIds - ); - } - }, 1000); - - timerId.current = loopId; - }, []); - - const expandClassName = classnames({ - 'import-tasks-step-content-header-expand': - serverDataImportStore.isExpandImportConfig, - 'import-tasks-step-content-header-collpase': !serverDataImportStore.isExpandImportConfig - }); - - useEffect(() => { - // if comes from import manager or refresh - if ( - (!serverDataImportStore.readOnly || - serverDataImportStore.isIrregularProcess) && - dataImportRootStore.currentStatus === 'LOADING' - ) { - loopQueryImportData(); - } - - return () => { - if (!Object.is(NaN, timerId.current)) { - window.clearInterval(timerId.current); - } - }; - }, [ - isInitReady, - serverDataImportStore.readOnly, - serverDataImportStore.isIrregularProcess, - dataImportRootStore.currentStatus - ]); - - return isInitReady ? ( -
      -
      - {t('server-data-import.import-settings.title')} - collpaseOrExpand { - serverDataImportStore.switchExpandImportConfig( - !serverDataImportStore.isExpandImportConfig - ); - }} - /> -
      - - {serverDataImportStore.isExpandImportConfig && ( -
      -
      -
      -
      - - {t('server-data-import.import-settings.checkIfExist')}: - - - hint - -
      -
      - { - serverDataImportStore.mutateImportConfigs( - 'check_vertex', - checked - ); - }} - disabled={ - serverDataImportStore.readOnly || - serverDataImportStore.importConfigReadOnly - } - /> -
      -
      -
      - - {t( - 'server-data-import.import-settings.maximumAnalyzedErrorRow' - )} - : - - {!serverDataImportStore.readOnly && - !serverDataImportStore.importConfigReadOnly ? ( - - ) : ( -
      - {serverDataImportStore.importConfigs?.max_parse_errors} -
      - )} -
      -
      - - {t( - 'server-data-import.import-settings.maxiumInterpolateErrorRow' - )} - : - - {!serverDataImportStore.readOnly && - !serverDataImportStore.importConfigReadOnly ? ( - - ) : ( -
      - {serverDataImportStore.importConfigs?.max_insert_errors} -
      - )} -
      -
      -
      -
      - - {t( - 'server-data-import.import-settings.requestTimesWhenInterpolationFailed' - )} - : - - {!serverDataImportStore.readOnly && - !serverDataImportStore.importConfigReadOnly ? ( - - ) : ( -
      - {serverDataImportStore.importConfigs?.retry_times} -
      - )} -
      -
      - - {t( - 'server-data-import.import-settings.requestTicksWhenInterpolationFailed' - )} - : - - {!serverDataImportStore.readOnly && - !serverDataImportStore.importConfigReadOnly ? ( - - ) : ( -
      - {serverDataImportStore.importConfigs?.retry_interval} -
      - )} -
      -
      - - {t('server-data-import.import-settings.InterpolationTimeout')}: - - {!serverDataImportStore.readOnly && - !serverDataImportStore.importConfigReadOnly ? ( - - ) : ( -
      - {serverDataImportStore.importConfigs?.insert_timeout} -
      - )} -
      -
      -
      - )} - - {(dataImportRootStore.currentStatus === 'LOADING' || - dataImportRootStore.currentStatus === 'SUCCESS' || - dataImportRootStore.currentStatus === 'FAILED' || - (serverDataImportStore.isIrregularProcess && - serverDataImportStore.readOnly)) && ( - <> -
      - {t('server-data-import.import-details.title')} -
      -
      -
    - - - )} - - {!serverDataImportStore.readOnly && ( -
    - - -
    - )} - - ) : null; -}); - -export interface ImportManipulationsProps { - importStatus: string; - taskIndex: number; - loopQuery: () => void; -} - -const ImportManipulations: React.FC = observer( - ({ importStatus, taskIndex, loopQuery }) => { - const dataImportRootStore = useContext(DataImportRootStoreContext); - const { serverDataImportStore } = dataImportRootStore; - const { t } = useTranslation(); - const manipulations: string[] = []; - - const handleClickManipulation = async (manipulation: string) => { - switch (manipulation) { - case t('server-data-import.import-details.manipulations.pause'): - await serverDataImportStore.pauseImport( - serverDataImportStore.importTasks[taskIndex].id - ); - - if (serverDataImportStore.requestStatus.pauseImport === 'failed') { - Message.error({ - content: serverDataImportStore.errorInfo.pauseImport.message, - size: 'medium', - showCloseIcon: false - }); - } - - break; - case t('server-data-import.import-details.manipulations.abort'): - await serverDataImportStore.abortImport( - serverDataImportStore.importTasks[taskIndex].id - ); - - if (serverDataImportStore.requestStatus.abortImport === 'failed') { - Message.error({ - content: serverDataImportStore.errorInfo.abortImport.message, - size: 'medium', - showCloseIcon: false - }); - } - - serverDataImportStore.fetchAllImportTasks(); - - break; - case t('server-data-import.import-details.manipulations.resume'): - await serverDataImportStore.resumeImport( - serverDataImportStore.importTasks[taskIndex].id - ); - - if (serverDataImportStore.requestStatus.resumeImport === 'failed') { - Message.error({ - content: serverDataImportStore.errorInfo.resumeImport.message, - size: 'medium', - showCloseIcon: false - }); - } - - serverDataImportStore.switchImporting(true); - serverDataImportStore.switchImportFinished(false); - loopQuery(); - break; - case t('server-data-import.import-details.manipulations.failed-cause'): - serverDataImportStore.checkErrorLogs( - dataImportRootStore.currentId!, - dataImportRootStore.currentJobId!, - serverDataImportStore.importTasks[taskIndex].id - ); - break; - case t('server-data-import.import-details.manipulations.retry'): - await serverDataImportStore.retryImport( - serverDataImportStore.importTasks[taskIndex].id - ); - - if (serverDataImportStore.requestStatus.retryImport === 'failed') { - Message.error({ - content: serverDataImportStore.errorInfo.retryImport.message, - size: 'medium', - showCloseIcon: false - }); - } - - serverDataImportStore.switchImporting(true); - serverDataImportStore.switchImportFinished(false); - loopQuery(); - break; - } - }; - - switch (importStatus) { - case 'RUNNING': - manipulations.push( - t('server-data-import.import-details.manipulations.pause'), - t('server-data-import.import-details.manipulations.abort') - ); - break; - case 'FAILED': - manipulations.push( - t('server-data-import.import-details.manipulations.resume'), - t('server-data-import.import-details.manipulations.failed-cause') - ); - break; - case 'PAUSED': - manipulations.push( - t('server-data-import.import-details.manipulations.resume'), - t('server-data-import.import-details.manipulations.abort') - ); - break; - case 'STOPPED': - manipulations.push( - t('server-data-import.import-details.manipulations.retry') - ); - break; - case 'SUCCEED': - break; - default: - throw new Error('Wrong status received from server'); - } - - return ( -
    - {manipulations.map((manipulation) => { - if ( - manipulation === - t('server-data-import.import-details.manipulations.failed-cause') - ) { - return ( - { - handleClickManipulation(manipulation); - }} - > - {manipulation} - - ); - } - - return ( - { - serverDataImportStore.switchImportFinished(false); - handleClickManipulation(manipulation); - }} - > - {manipulation} - - ); - })} -
    - ); - } -); - -export default ImportConfigs; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ServerDataImport.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ServerDataImport.less deleted file mode 100644 index 7ba8506e1..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ServerDataImport.less +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.import-tasks { - &-server-data-import { - &-configs-wrapper { - width: 100%; - margin-bottom: 30px; - } - - &-configs { - display: flex; - width: 100%; - } - - &-config { - height: 128px; - display: flex; - justify-content: space-between; - flex-direction: column; - flex-basis: 50%; - - &:first-child { - align-items: flex-end; - // margin-right: 87px; - } - - &:last-child { - align-items: flex-start; - } - } - - &-config-option { - display: flex; - align-items: center; - - & > div:first-child, - & > span:first-child { - width: 200.7px; - margin-right: 44px; - text-align: right; - color: #333; - font-size: 14px; - } - - &-readonly-data { - width: 100px; - font-size: 14px; - color: #333; - line-height: 20px; - } - } - - &-manipulations { - width: 100%; - margin: 40px 0 6px; - display: flex; - justify-content: center; - } - - &-table-wrapper { - & table { - table-layout: fixed; - } - } - - &-table-tooltip-title { - width: 234px; - } - - &-table-progress { - position: relative; - width: 80%; - height: 8px; - background: #eee; - border-radius: 4px; - margin-right: 8px; - } - - &-table-manipulations { - display: flex; - // justify-content: space-between; - } - } -} - -/* override */ - -.import-tasks-server-data-import-config-option .new-fc-one-input-error.new-fc-one-input-error-layer { - // safari bug - // width: max-content; - word-break: keep-all; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ServerDataImport.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ServerDataImport.tsx deleted file mode 100644 index b84fb2870..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/ServerDataImport.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import { observer } from 'mobx-react'; - -import ImportConfigs from './ImportConfigs'; - -import './ServerDataImport.less'; - -const ServerDataImport: React.FC = observer(() => { - return ( -
    - -
    - ); -}); - -export default ServerDataImport; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/index.ts deleted file mode 100644 index 850e2c888..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/import-tasks/server-data-import/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import ServerDataImport from './ServerDataImport'; - -export { ServerDataImport }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/index.ts deleted file mode 100644 index 6ff89dc6e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/data-import/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import { ImportTasks, ImportManager, JobDetails } from './import-tasks'; - -export { ImportTasks, ImportManager, JobDetails }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/index.ts deleted file mode 100644 index c92bd3ece..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import GraphManagement from './GraphManagement'; -import { DataAnalyze } from './data-analyze'; -import { MetadataConfigs } from './metadata-configs'; -import { ImportTasks, ImportManager, JobDetails } from './data-import'; -import { AsyncTaskList } from './async-tasks'; - -export { - GraphManagement, - DataAnalyze, - MetadataConfigs, - ImportTasks, - ImportManager, - JobDetails, - AsyncTaskList -}; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/MetadataConfigs.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/MetadataConfigs.less deleted file mode 100644 index f5a0cf5bb..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/MetadataConfigs.less +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.metadata-configs { - position: absolute; - width: calc(100% - 60px); - padding: 0 16px 16px 16px; - left: 60px; - top: 60px; - - &-with-expand-sidebar { - width: calc(100% - 200px); - left: 200px; - } - - &-content { - &-wrapper { - margin-top: 16px; - padding: 16px; - background: #fff; - border: 1px solid #eee; - border-radius: 3px; - } - - &-loading-wrapper { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - &-loading-bg { - width: 144px; - height: 144px; - position: relative; - margin-bottom: 10px; - } - - &-loading-back { - position: absolute; - left: 22px; - top: 20px; - } - - &-loading-front { - position: absolute; - left: 57.1px; - top: 52.1px; - animation: loading-rotate 2s linear infinite; - } - - &-mode { - margin: 16px 0; - - &-button { - display: flex; - align-items: center; - - & > img { - margin-right: 6px; - } - } - } - - &-header { - margin-bottom: 16px; - display: flex; - justify-content: flex-end; - } - - &-dropdown { - width: 382px; - max-height: 332px; - overflow: auto; - background: #fff; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - color: #333; - - & > div { - padding: 0 16px; - height: 32px; - display: flex; - align-items: center; - } - } - } - - &-drawer { - font-size: 14px; - margin-top: 4px; - } - - &-sorted-multiSelect-option { - display: flex; - - & > div:first-child { - margin: 8px 8px 0; - width: 16px; - height: 16px; - border: 1px solid #ccc; - background: #fff; - border-radius: 2px; - } - - & > div:last-child { - width: 80%; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - &-selected { - & > div:first-child { - margin: 8px 8px 0; - width: 16px; - height: 16px; - background: #2b65ff; - border-radius: 2px; - text-align: center; - line-height: 14px; - color: #fff; - } - } - } -} - -.metadata-properties-tooltips { - background: #fff; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - padding: 24px; - font-size: 14px; - color: #333; - - &-title { - font-size: 16px; - font-weight: 900; - margin-bottom: 16px; - } - - &-footer { - display: flex; - margin-top: 24px; - } - - // & p { - // margin: 0; - // line-height: 28px; - // } -} - -.metadata-title { - font-family: - 'PingFangSC-Medium', - 'Microsoft YaHei', - '微软雅黑', - Arial, - sans-serif; - font-weight: bold; - font-size: 16px; - color: #000; - letter-spacing: 0; - line-height: 24px; -} - -.metadata-drawer { - &-options { - margin-bottom: 12px; - display: flex; - - &-disabled { - color: #ccc; - } - - &-name { - width: 98px; - text-align: right; - margin-right: 9px; - } - - &-list { - width: calc(100% - 155px); - - &-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - - &-normal { - & > div, - & > span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &:nth-child(1) { - flex: 0 1 30%; - } - - &:nth-child(2) { - flex: 0 1 32%; - } - - &:nth-child(3) { - flex: 0 1 38%; - display: flex; - align-items: center; - - & > img { - margin-left: 4px; - } - } - } - } - - &:last-child { - margin-bottom: 0; - } - } - } - } -} - -.metadata-selected-properties { - display: flex; - justify-content: space-between; - margin: 12px 0; -} - -.metdata-essential-form-options { - color: #fb4b53; -} - -// override -.metadata-configs { - table { - table-layout: fixed; - } - - .new-fc-one-table { - overflow: visible; - border: 0; - } - - .new-fc-one-table table { - overflow: visible; - } - - .new-fc-one-menu { - background: transparent; - } - - .new-fc-one-select-dropdown-menu-item.new-fc-one-select-dropdown-menu-item-selected { - color: #2b65ff; - font-weight: 900; - } -} - -// override -.metadata-drawer-options-list-row { - .new-fc-one-select-search-ul { - .new-fc-one-select-search.new-fc-one-select-search--inline { - display: none; - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/MetadataConfigs.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/MetadataConfigs.tsx deleted file mode 100644 index e0c9ce130..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/MetadataConfigs.tsx +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useState, useEffect, useContext } from 'react'; -import { observer } from 'mobx-react'; -import { useRoute, useLocation, Params } from 'wouter'; -import classnames from 'classnames'; -import { AnimatePresence } from 'framer-motion'; -import { Radio, Menu, Modal, Button } from 'hubble-ui'; - -import { MetadataProperties } from './property'; -import { VertexTypeList } from './vertex-type'; -import { EdgeTypeList } from './edge-type'; -import { PropertyIndex } from './property-index'; -import { GraphView } from './graph-view'; -import DataAnalyzeStore from '../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import MetadataConfigsRootStore from '../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import './MetadataConfigs.less'; -import { AppStoreContext, GraphManagementStoreContext } from '../../../stores'; -import ActiveTableIcon from '../../../assets/imgs/ic_liebiaomoshi_white.svg'; -import TableIcon from '../../../assets/imgs/ic_liebiaomoshi_black.svg'; -import ActiveShowGraphIcon from '../../../assets/imgs/ic_tumoshi_white.svg'; -import ShowGraphIcon from '../../../assets/imgs/ic_tumoshi_black.svg'; -import { useTranslation } from 'react-i18next'; - -const MetadataConfig: React.FC = observer(() => { - const appStore = useContext(AppStoreContext); - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const graphManagementStore = useContext(GraphManagementStoreContext); - const metadataConfigRootStore = useContext(MetadataConfigsRootStore); - const [viewMode, setViewMode] = useState('list'); - const [selectedMenuItem, setSelectedMenuItem] = useState('property'); - const [match, params] = useRoute('/graph-management/:id/metadata-configs'); - const [_, setLocation] = useLocation(); - const { t } = useTranslation(); - - const handleRadioGroupChange = (e: React.ChangeEvent) => { - setViewMode(e.target.value); - }; - - const handleMenuItemChange = ({ key }: { key: string }) => { - // reset store current tab status to default - switch (selectedMenuItem) { - case 'property': - metadataConfigRootStore.metadataPropertyStore.changeCurrentTabStatus( - 'list' - ); - case 'vertex-type': - metadataConfigRootStore.vertexTypeStore.changeCurrentTabStatus('list'); - case 'edge-type': - metadataConfigRootStore.edgeTypeStore.changeCurrentTabStatus('list'); - } - - setSelectedMenuItem(key); - }; - - const wrapperClassName = classnames({ - 'metadata-configs': true, - 'metadata-configs-with-expand-sidebar': graphManagementStore.isExpanded - }); - - const renderListView = () => { - switch (selectedMenuItem) { - case 'property': - return ; - case 'vertex-type': - return ; - case 'edge-type': - return ; - case 'property-index': - return ; - } - }; - - // Caution: Preitter will automatically add 'params' behind 'match' in array, - // which is not equal each time - /* eslint-disable */ - useEffect(() => { - window.scrollTo(0, 0); - graphManagementStore.fetchIdList(); - - if (match && params !== null) { - appStore.setCurrentId(Number(params.id)); - // fetch node colors - dataAnalyzeStore.setCurrentId(Number(params.id)); - dataAnalyzeStore.fetchAllNodeStyle(); - dataAnalyzeStore.fetchAllEdgeStyle(); - metadataConfigRootStore.setCurrentId(Number(params.id)); - // metadataConfigRootStore.fetchIdList(); - } - - return () => { - metadataConfigRootStore.dispose(); - }; - }, [metadataConfigRootStore, match, params?.id]); - - return ( -
    -
    -
    - - -
    - table mode - {t('addition.menu.list-mode')} -
    -
    - -
    - graph mode - {t('addition.menu.chart-mode')} -
    -
    -
    -
    - {viewMode === 'list' ? ( - <> - - - {t('addition.common.property')} - - - {t('addition.common.vertex-type')} - - - {t('addition.common.edge-type')} - - - {t('addition.common.property-index')} - - - - {renderListView()} - - - ) : ( - - )} -
    - { - metadataConfigRootStore.setCurrentId(null); - setLocation('/'); - }} - > - {t('addition.dataAnalyze.return-home')} - - ]} - visible={graphManagementStore.graphData.some( - ({ id, enabled }) => - metadataConfigRootStore.currentId === id && !enabled - )} - destroyOnClose - needCloseIcon={false} - > -
    - {metadataConfigRootStore.metadataPropertyStore.errorMessage} -
    -
    -
    - ); -}); - -export default MetadataConfig; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/common/EmptyDataView.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/common/EmptyDataView.tsx deleted file mode 100644 index cc38c32eb..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/common/EmptyDataView.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import { observer } from 'mobx-react'; - -import LoadingBackIcon from '../../../../assets/imgs/ic_loading_back.svg'; -import LoadingFrontIcon from '../../../../assets/imgs/ic_loading_front.svg'; - -import i18next from '../../../../i18n'; -export interface LoadingDataViewProps { - isLoading: boolean; - emptyText?: string; -} - -const LoadingDataView: React.FC = observer( - ({ isLoading, emptyText }) => - isLoading ? ( -
    -
    - load background - load spinner -
    - {i18next.t('addition.message.data-loading')}... -
    - ) : emptyText ? ( - {emptyText} - ) : null -); - -export default LoadingDataView; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/EdgeTypeList.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/EdgeTypeList.less deleted file mode 100644 index a6874e139..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/EdgeTypeList.less +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.edge-type-list-wrapper { - width: 100%; - background: #fff; - margin-top: 16px; - padding: 16px; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/EdgeTypeList.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/EdgeTypeList.tsx deleted file mode 100644 index a29c32ddd..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/EdgeTypeList.tsx +++ /dev/null @@ -1,1957 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import { - isEmpty, - isUndefined, - cloneDeep, - intersection, - size, - without -} from 'lodash-es'; -import { useLocation } from 'wouter'; -import classnames from 'classnames'; -import { motion } from 'framer-motion'; -import { - Button, - Table, - Switch, - Modal, - Drawer, - Input, - Select, - Checkbox, - Message, - Loading -} from 'hubble-ui'; - -import { Tooltip, LoadingDataView } from '../../../common'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; - -import NewEdgeType from './NewEdgeType'; -import ReuseEdgeTypes from './ReuseEdgeTypes'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { formatVertexIdText } from '../../../../stores/utils'; - -import type { - EdgeTypeValidatePropertyIndexes, - EdgeType -} from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import AddIcon from '../../../../assets/imgs/ic_add.svg'; -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import WhiteCloseIcon from '../../../../assets/imgs/ic_close_white.svg'; -import CloseIcon from '../../../../assets/imgs/ic_close_16.svg'; -import LoadingBackIcon from '../../../../assets/imgs/ic_loading_back.svg'; -import LoadingFrontIcon from '../../../../assets/imgs/ic_loading_front.svg'; -import SelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow_selected.svg'; -import NoSelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow.svg'; -import SelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight_selected.svg'; -import NoSelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight.svg'; -import i18next from '../../../../i18n'; -import { useTranslation } from 'react-i18next'; - -import './EdgeTypeList.less'; - -const styles = { - button: { - marginLeft: 12, - width: 78 - }, - header: { - marginBottom: 16 - }, - manipulation: { - marginRight: 12 - }, - deleteWrapper: { - display: 'flex', - justifyContent: 'flex-end' - }, - loading: { - padding: 0, - marginRight: 4 - } -}; - -const variants = { - initial: { - opacity: 0 - }, - animate: { - opacity: 1, - transition: { - duration: 0.7 - } - }, - exit: { - opacity: 0, - transition: { - duration: 0.3 - } - } -}; - -const propertyIndexTypeMappings: Record = { - SECONDARY: i18next.t('addition.menu.secondary-index'), - RANGE: i18next.t('addition.menu.range-index'), - SEARCH: i18next.t('addition.menu.full-text-index') -}; - -const EdgeTypeList: React.FC = observer(() => { - const { t } = useTranslation(); - const metadataConfigsRootStore = useContext(MetadataConfigsRootStore); - const { metadataPropertyStore, edgeTypeStore } = metadataConfigsRootStore; - const [preLoading, switchPreLoading] = useState(true); - const [sortOrder, setSortOrder] = useState(''); - const [selectedRowKeys, mutateSelectedRowKeys] = useState([]); - const [isShowModal, switchShowModal] = useState(false); - const [isAddProperty, switchIsAddProperty] = useState(false); - const [isEditEdge, switchIsEditEdge] = useState(false); - const [ - deleteExistPopIndexInDrawer, - setDeleteExistPopIndexInDrawer - ] = useState(null); - const [ - deleteAddedPopIndexInDrawer, - setDeleteAddedPopIndexInDrawer - ] = useState(null); - const [, setLocation] = useLocation(); - - const dropdownWrapperRef = useRef(null); - const deleteWrapperInDrawerRef = useRef(null); - - const isLoading = - preLoading || edgeTypeStore.requestStatus.fetchEdgeTypeList === 'pending'; - - const currentSelectedRowKeys = intersection( - selectedRowKeys, - edgeTypeStore.edgeTypes.map(({ name }) => name) - ); - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isAddProperty && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddProperty(false); - } - - if ( - (deleteExistPopIndexInDrawer || deleteAddedPopIndexInDrawer) && - deleteWrapperInDrawerRef.current && - !deleteWrapperInDrawerRef.current.contains(e.target as Element) - ) { - setDeleteExistPopIndexInDrawer(null); - setDeleteAddedPopIndexInDrawer(null); - } - }, - [deleteExistPopIndexInDrawer, deleteWrapperInDrawerRef, isAddProperty] - ); - - const handleSelectedTableRow = (newSelectedRowKeys: string[]) => { - mutateSelectedRowKeys(newSelectedRowKeys); - }; - - const handleCloseDrawer = () => { - switchIsAddProperty(false); - switchIsEditEdge(false); - edgeTypeStore.selectEdgeType(null); - edgeTypeStore.resetEditedSelectedEdgeType(); - }; - - const handleSortClick = () => { - switchPreLoading(true); - - if (sortOrder === 'descend') { - edgeTypeStore.mutatePageSort('asc'); - setSortOrder('ascend'); - } else { - edgeTypeStore.mutatePageSort('desc'); - setSortOrder('descend'); - } - - edgeTypeStore.fetchEdgeTypeList(); - }; - - const batchDeleteProperties = async () => { - switchShowModal(false); - // need to set a copy in store since local row key state would be cleared - edgeTypeStore.mutateSelectedEdgeTypeNames(currentSelectedRowKeys); - // mutateSelectedRowKeys([]); - await edgeTypeStore.deleteEdgeType(currentSelectedRowKeys); - // edgeTypeStore.mutateSelectedEdgeTypeNames([]); - - if (edgeTypeStore.requestStatus.deleteEdgeType === 'success') { - Message.success({ - content: t('addition.common.del-success'), - size: 'medium', - showCloseIcon: false - }); - - mutateSelectedRowKeys( - without(selectedRowKeys, ...currentSelectedRowKeys) - ); - - await edgeTypeStore.fetchEdgeTypeList(); - - // fetch previous page data if it's empty - if ( - edgeTypeStore.requestStatus.fetchEdgeTypeList === 'success' && - size(edgeTypeStore.edgeTypes) === 0 && - edgeTypeStore.edgeTypeListPageConfig.pageNumber > 1 - ) { - edgeTypeStore.mutatePageNumber( - edgeTypeStore.edgeTypeListPageConfig.pageNumber - 1 - ); - - edgeTypeStore.fetchEdgeTypeList(); - } - - return; - } - - if (edgeTypeStore.requestStatus.deleteEdgeType === 'failed') { - Message.error({ - content: edgeTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - } - }; - - const columnConfigs = [ - { - title: t('addition.common.edge-type-name'), - dataIndex: 'name', - sorter: true, - sortOrder, - width: '14%', - render(text: string, records: any[], index: number) { - return ( -
    { - edgeTypeStore.selectEdgeType(index); - - // check also need style infos - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - style: { - color: edgeTypeStore.selectedEdgeType!.style.color, - icon: null, - with_arrow: edgeTypeStore.selectedEdgeType!.style.with_arrow, - thickness: edgeTypeStore.selectedEdgeType!.style.thickness, - display_fields: edgeTypeStore.selectedEdgeType!.style - .display_fields - } - }); - }} - > - {text} -
    - ); - } - }, - { - title: t('addition.common.source-type'), - dataIndex: 'source_label', - width: '14%', - render(text: string, records: any[], index: number) { - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.common.target-type'), - dataIndex: 'target_label', - width: '14%', - render(text: string, records: any[], index: number) { - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.common.association-property'), - dataIndex: 'properties', - width: '14%', - render(properties: { name: string; nullable: boolean }[]) { - const text = - properties.length !== 0 - ? properties.map(({ name }) => name).join('; ') - : '-'; - - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.common.distinguishing-key'), - dataIndex: 'sort_keys', - width: '10%', - render(values: string[]) { - const text = values.length !== 0 ? values.join(';') : '-'; - - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.menu.type-index'), - dataIndex: 'open_label_index', - width: '8%', - render(value: boolean) { - return ( - - ); - } - }, - { - title: t('addition.common.property-index'), - dataIndex: 'property_indexes', - width: '14%', - render(indexes: { name: string; type: string; fields: string[] }[]) { - const text = - indexes.length !== 0 - ? indexes.map(({ name }) => name).join('; ') - : '-'; - - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '12%', - render(_: any, records: EdgeType, index: number) { - return ( - - ); - } - } - ]; - - const metadataDrawerOptionClass = classnames({ - 'metadata-drawer-options': true, - 'metadata-drawer-options-disabled': isEditEdge - }); - - useEffect(() => { - setTimeout(() => { - switchPreLoading(false); - }, 500); - }, [sortOrder]); - - useEffect(() => { - return () => { - const messageComponents = document.querySelectorAll( - '.new-fc-one-message' - ) as NodeListOf; - - messageComponents.forEach((messageComponent) => { - messageComponent.style.display = 'none'; - }); - }; - }); - - useEffect(() => { - if (metadataConfigsRootStore.currentId !== null) { - metadataPropertyStore.fetchMetadataPropertyList({ fetchAll: true }); - edgeTypeStore.fetchEdgeTypeList(); - } - - return () => { - edgeTypeStore.dispose(); - }; - }, [ - metadataPropertyStore, - metadataConfigsRootStore.currentId, - edgeTypeStore - ]); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - if (edgeTypeStore.currentTabStatus === 'new') { - return ; - } - - if (edgeTypeStore.currentTabStatus === 'reuse') { - return ; - } - - return ( - -
    -
    - - -
    - {size(currentSelectedRowKeys) !== 0 && ( -
    -
    - {t('addition.message.selected')} - {size(currentSelectedRowKeys)} - {t('addition.common.term')} -
    - - {t('addition.common.close')} { - mutateSelectedRowKeys([]); - }} - /> -
    - )} -
    rowData.name} - locale={{ - emptyText: ( - } - /> - ) - }} - rowSelection={{ - selectedRowKeys, - onChange: handleSelectedTableRow - }} - onSortClick={handleSortClick} - dataSource={isLoading ? [] : edgeTypeStore.edgeTypes} - pagination={ - isLoading - ? null - : { - hideOnSinglePage: false, - pageNo: edgeTypeStore.edgeTypeListPageConfig.pageNumber, - pageSize: 10, - showSizeChange: false, - showPageJumper: false, - total: edgeTypeStore.edgeTypeListPageConfig.pageTotal, - onPageNoChange: (e: React.ChangeEvent) => { - // mutateSelectedRowKeys([]); - edgeTypeStore.mutatePageNumber(Number(e.target.value)); - edgeTypeStore.fetchEdgeTypeList(); - } - } - } - /> - { - switchShowModal(false); - }} - > -
    -
    - {t('addition.common.del-comfirm')} - {t('addition.common.close')} { - switchShowModal(false); - }} - /> -
    -
    - {t('addition.message.edge-del-confirm')} -
    -
    - {t('addition.message.long-time-notice')} -
    -
    ) { - return ( - - {text} - - ); - } - } - ]} - dataSource={currentSelectedRowKeys.map((name) => ({ - name - }))} - pagination={false} - /> - - - { - if (!isEditEdge) { - switchIsEditEdge(true); - edgeTypeStore.validateEditEdgeType(); - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - style: { - color: edgeTypeStore.selectedEdgeType!.style.color, - icon: null, - with_arrow: edgeTypeStore.selectedEdgeType!.style - .with_arrow, - thickness: edgeTypeStore.selectedEdgeType!.style - .thickness, - display_fields: edgeTypeStore.selectedEdgeType!.style - .display_fields - } - }); - } else { - await edgeTypeStore.updateEdgeType(); - - if (edgeTypeStore.requestStatus.updateEdgeType === 'failed') { - Message.error({ - content: edgeTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - - return; - } - - if ( - edgeTypeStore.requestStatus.updateEdgeType === 'success' - ) { - if ( - isEmpty( - edgeTypeStore.editedSelectedEdgeType - .append_property_indexes - ) - ) { - Message.success({ - content: t('addition.operate.modify-success'), - size: 'medium', - showCloseIcon: false - }); - } else { - Message.success({ - content: ( -
    -
    - {t('addition.common.save-scuccess')} -
    -
    - {t('addition.message.index-long-time-notice')} -
    -
    { - setLocation( - `/graph-management/${metadataConfigsRootStore.currentId}/async-tasks` - ); - }} - > - {t('addition.operate.view-task-management')} -
    -
    - ), - duration: 60 * 60 * 24 - }); - } - } - - switchIsEditEdge(false); - edgeTypeStore.selectEdgeType(null); - edgeTypeStore.fetchEdgeTypeList(); - edgeTypeStore.resetEditedSelectedEdgeType(); - } - }} - > - {isEditEdge - ? t('addition.common.save') - : t('addition.common.edit')} - , - - ]} - > - {!isEmpty(edgeTypeStore.selectedEdgeType) && ( -
    -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - {t('addition.common.edge-type-name')}: -
    -
    - {edgeTypeStore.selectedEdgeType!.name} -
    -
    -
    -
    - - {t('addition.common.edge-style')}: - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - {t('addition.common.source-type')}: -
    -
    - {edgeTypeStore.selectedEdgeType!.source_label} -
    -
    -
    -
    - {t('addition.common.target-type')}: -
    -
    - {edgeTypeStore.selectedEdgeType!.target_label} -
    -
    -
    -
    - - {t('addition.common.allow-multiple-connections')}: - -
    - -
    -
    -
    - {t('addition.common.association-property')}: -
    -
    -
    - {t('addition.common.property')} - {t('addition.common.allow-null')} -
    - {edgeTypeStore.selectedEdgeType!.properties.map( - ({ name, nullable }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditEdge && - edgeTypeStore.editedSelectedEdgeType.append_properties.map( - ({ name }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditEdge && ( -
    { - switchIsAddProperty(!isAddProperty); - }} - > - - {t('addition.common.add-property')} - - toogleAddProperties -
    - )} - {isEditEdge && isAddProperty && ( -
    - {metadataPropertyStore.metadataProperties - .filter( - (property) => - edgeTypeStore.selectedEdgeType!.properties.find( - ({ name }) => name === property.name - ) === undefined - ) - .map((property) => ( -
    - - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesInSelectedVertextType = - edgeTypeStore.addedPropertiesInSelectedEdgeType; - - addedPropertiesInSelectedVertextType.has( - property.name - ) - ? addedPropertiesInSelectedVertextType.delete( - property.name - ) - : addedPropertiesInSelectedVertextType.add( - property.name - ); - - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - append_properties: [ - ...addedPropertiesInSelectedVertextType - ].map((propertyName) => { - const currentProperty = edgeTypeStore.newEdgeType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined( - currentProperty - ) - ? currentProperty.nullable - : true - }; - }) - }); - }} - > - {property.name} - - -
    - ))} -
    - )} -
    -
    -
    -
    - - {t('addition.common.distinguishing-key-property')}: - -
    -
    - {edgeTypeStore.selectedEdgeType!.sort_keys.join(';')} -
    -
    -
    -
    - - {t('addition.edge.display-content')}: - -
    - {isEditEdge ? ( - - ) : ( -
    - {edgeTypeStore.selectedEdgeType?.style.display_fields - .map((field) => - formatVertexIdText( - field, - t('addition.function-parameter.edge-type') - ) - ) - .join('-')} -
    - )} -
    -
    - {t('addition.edge.index-info')} -
    -
    -
    - {t('addition.menu.type-index')}: -
    - -
    -
    -
    - {t('addition.common.property-index')}: -
    -
    - {(edgeTypeStore.selectedEdgeType!.property_indexes - .length !== 0 || - edgeTypeStore.editedSelectedEdgeType - .append_property_indexes.length !== 0) && ( -
    - {t('addition.edge.index-name')} - {t('addition.edge.index-type')} - {t('addition.common.property')} -
    - )} - {edgeTypeStore - .selectedEdgeType!.property_indexes.filter( - (propertyIndex) => - isUndefined( - edgeTypeStore.editedSelectedEdgeType.remove_property_indexes.find( - (removedPropertyName) => - removedPropertyName === propertyIndex.name - ) - ) - ) - .map(({ name, type, fields }, index) => { - return ( -
    -
    {name}
    -
    {propertyIndexTypeMappings[type]}
    -
    - - {fields - .map( - (field, index) => index + 1 + '.' + field - ) - .join(';')} - - - {isEditEdge && ( - -

    - {t( - 'addition.message.property-del-confirm' - )} -

    -

    - {t( - 'addition.message.index-del-confirm' - )} -

    -
    -
    { - const removedPropertyIndex = cloneDeep( - edgeTypeStore - .editedSelectedEdgeType - .remove_property_indexes - ); - - removedPropertyIndex.push( - edgeTypeStore.selectedEdgeType! - .property_indexes[index].name - ); - - edgeTypeStore.mutateEditedSelectedEdgeType( - { - ...edgeTypeStore.editedSelectedEdgeType, - remove_property_indexes: removedPropertyIndex - } - ); - - setDeleteExistPopIndexInDrawer( - null - ); - edgeTypeStore.validateEditEdgeType( - true - ); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeleteExistPopIndexInDrawer( - null - ); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteExistPopIndexInDrawer(index); - } - }} - childrenWrapperElement="img" - /> - )} -
    -
    - ); - })} - {edgeTypeStore.editedSelectedEdgeType.append_property_indexes.map( - ({ name, type, fields }, index) => { - return ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - edgeTypeStore.editedSelectedEdgeType - .append_property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - append_property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - edgeTypeStore.validateEditEdgeType(); - } - }} - /> -
    -
    - -
    -
    - - - -

    - {t( - 'addition.message.property-del-confirm' - )} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const appendPropertyIndexes = cloneDeep( - edgeTypeStore.editedSelectedEdgeType! - .append_property_indexes - ); - - appendPropertyIndexes.splice( - index, - 1 - ); - - edgeTypeStore.mutateEditedSelectedEdgeType( - { - ...edgeTypeStore.editedSelectedEdgeType, - append_property_indexes: appendPropertyIndexes - } - ); - - setDeleteAddedPopIndexInDrawer(null); - edgeTypeStore.validateEditEdgeType( - true - ); - }} - > - {t('addition.common.confirm')} -
    -
    { - edgeTypeStore.resetEditedSelectedEdgeType(); - setDeleteAddedPopIndexInDrawer(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteAddedPopIndexInDrawer(index); - } - }} - childrenWrapperElement="img" - /> -
    -
    - ); - } - )} - {isEditEdge && ( - { - if ( - edgeTypeStore.editedSelectedEdgeType - .append_property_indexes.length === 0 || - edgeTypeStore.isEditReady - ) { - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - append_property_indexes: [ - ...edgeTypeStore.editedSelectedEdgeType - .append_property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - edgeTypeStore.validateEditEdgeType(true); - } - }} - style={{ - cursor: 'pointer', - color: edgeTypeStore.isEditReady ? '#2b65ff' : '#999' - }} - > - {t('addition.edge.add-group')} - - )} -
    -
    - - - )} -
    - - - ); -}); - -export interface EdgeTypeListManipulation { - edgeName: string; - edgeIndex: number; - switchIsEditEdge: (flag: boolean) => void; -} - -const EdgeTypeListManipulation: React.FC = observer( - ({ edgeName, edgeIndex, switchIsEditEdge }) => { - const { edgeTypeStore } = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - const [isPopDeleteModal, switchPopDeleteModal] = useState(false); - const [isDeleting, switchDeleting] = useState(false); - const deleteWrapperRef = useRef(null); - const isDeleteOrBatchDeleting = - isDeleting || - (edgeTypeStore.requestStatus.deleteEdgeType === 'pending' && - edgeTypeStore.selectedEdgeTypeNames.includes(edgeName)); - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isPopDeleteModal && - deleteWrapperRef && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchPopDeleteModal(false); - } - }, - [deleteWrapperRef, isPopDeleteModal] - ); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( -
    - { - edgeTypeStore.selectEdgeType(edgeIndex); - edgeTypeStore.validateEditEdgeType(true); - switchIsEditEdge(true); - - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - style: { - color: edgeTypeStore.selectedEdgeType!.style.color, - icon: null, - with_arrow: edgeTypeStore.selectedEdgeType!.style.with_arrow, - thickness: edgeTypeStore.selectedEdgeType!.style.thickness, - display_fields: edgeTypeStore.selectedEdgeType!.style - .display_fields - } - }); - }} - > - {t('addition.common.edit')} - -
    - {isDeleteOrBatchDeleting && ( - - )} - -

    - {t('addition.edge.confirm-del-edge-type')} -

    -

    {t('addition.edge.confirm-del-edge-type-again')}

    -

    {t('addition.message.long-time-notice')}

    -
    - - -
    -
    - } - childrenProps={{ - className: 'metadata-properties-manipulation', - title: isDeleteOrBatchDeleting - ? t('addition.operate.del-ing') - : t('addition.common.del'), - onClick() { - if (isDeleteOrBatchDeleting) { - return; - } - - switchPopDeleteModal(true); - } - }} - > - {isDeleteOrBatchDeleting - ? t('addition.operate.del-ing') - : t('addition.common.del')} - -
    - - ); - } -); - -const EmptyEdgeTypeHints: React.FC = observer(() => { - const { edgeTypeStore } = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - return ( -
    - Add new property -
    - {t('addition.edge.no-edge-desc')} -
    -
    - - -
    -
    - ); -}); - -export default EdgeTypeList; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/NewEdgeType.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/NewEdgeType.less deleted file mode 100644 index db945395a..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/NewEdgeType.less +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.new-edge-type-wrapper { - width: 100%; - background: #fff; - margin-top: 16px; - padding: 16px; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/NewEdgeType.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/NewEdgeType.tsx deleted file mode 100644 index 4864443af..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/NewEdgeType.tsx +++ /dev/null @@ -1,1099 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { isUndefined, cloneDeep } from 'lodash-es'; -import { - Input, - Select, - Button, - Switch, - Tooltip, - Checkbox, - Message -} from 'hubble-ui'; - -import { Tooltip as CustomTooltip } from '../../../common'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { formatVertexIdText } from '../../../../stores/utils'; - -import type { EdgeTypeValidatePropertyIndexes } from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import HintIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import SelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow_selected.svg'; -import NoSelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow.svg'; -import SelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight_selected.svg'; -import NoSelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight.svg'; -import closeIcon from '../../../../assets/imgs/ic_close_16.svg'; -import { useTranslation } from 'react-i18next'; - -const NewVertexType: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const { t } = useTranslation(); - const { metadataPropertyStore, vertexTypeStore, edgeTypeStore } = useContext( - MetadataConfigsRootStore - ); - const [isAddNewProperty, switchIsAddNewProperty] = useState(false); - const [deletePopIndex, setDeletePopIndex] = useState(null); - const dropdownWrapperRef = useRef(null); - const deleteWrapperRef = useRef(null); - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isAddNewProperty && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddNewProperty(false); - } - - if ( - deletePopIndex && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - setDeletePopIndex(null); - } - }, - [deletePopIndex, isAddNewProperty] - ); - - useEffect(() => { - metadataPropertyStore.fetchMetadataPropertyList({ fetchAll: true }); - vertexTypeStore.fetchVertexTypeList({ fetchAll: true }); - edgeTypeStore.validateAllNewEdgeType(true); - }, [edgeTypeStore, metadataPropertyStore, vertexTypeStore]); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( -
    -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - * - {t('addition.common.edge-type-name')}: -
    - { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - name: e.value - }); - }} - originInputProps={{ - onBlur() { - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType('name'); - } - }} - /> -
    -
    -
    - * - {t('addition.common.edge-style')}: -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - * - {t('addition.common.source-type')}: -
    - -
    -
    -
    - * - {t('addition.common.target-type')}: -
    - -
    -
    -
    - * - {t('addition.common.allow-multiple-connections')}: - - hint - -
    - { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - link_multi_times: checked - }); - }} - /> -
    -
    -
    - {edgeTypeStore.newEdgeType.link_multi_times && ( - * - )} - {t('addition.common.association-property')}: -
    -
    - {edgeTypeStore.newEdgeType.properties.length !== 0 && ( -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.allow-null')}
    -
    - {edgeTypeStore.newEdgeType.properties.map((property, index) => { - const currentProperties = cloneDeep( - edgeTypeStore.newEdgeType.properties - ); - - return ( -
    -
    {property.name}
    -
    - { - currentProperties[index].nullable = - !currentProperties[index].nullable; - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - properties: currentProperties - }); - - // remove primary keys since it could be empty value - if (checked) { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - sort_keys: - edgeTypeStore.newEdgeType.sort_keys.filter( - (key) => key !== property.name - ) - }); - } - }} - size="large" - /> -
    -
    - ); - })} -
    - )} -
    { - switchIsAddNewProperty(!isAddNewProperty); - }} - > - {t('addition.common.add-property')} - toggleAddProperty -
    -
    -
    - {isAddNewProperty && ( -
    -
    -
    - {metadataPropertyStore.metadataProperties.map((property) => ( -
    - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesIndexInSelectedEdgeType = - edgeTypeStore.addedPropertiesInSelectedEdgeType; - - addedPropertiesIndexInSelectedEdgeType.has( - property.name - ) - ? addedPropertiesIndexInSelectedEdgeType.delete( - property.name - ) - : addedPropertiesIndexInSelectedEdgeType.add( - property.name - ); - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - properties: [ - ...addedPropertiesIndexInSelectedEdgeType - ].map((propertyName) => { - const currentProperty = - edgeTypeStore.newEdgeType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined(currentProperty) - ? currentProperty.nullable - : true - }; - }) - }); - - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType('properties'); - }} - > - {property.name} - - -
    - ))} -
    -
    - )} - - {edgeTypeStore.newEdgeType.link_multi_times && ( -
    -
    - * - {t('addition.common.distinguishing-key')}: -
    - -
    - )} - -
    -
    - * - {t('addition.edge.display-content')}: -
    - -
    - -
    - - {t('addition.edge.index-info')} - - - hint - -
    -
    -
    - * - {t('addition.menu.type-index')}: -
    - { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - open_label_index: checked - }); - }} - /> -
    - -
    -
    - {t('addition.common.property-index')}: -
    -
    - {edgeTypeStore.newEdgeType.property_indexes.length !== 0 && ( -
    -
    - {t('addition.edge.index-name')} -
    -
    - {t('addition.edge.index-type')} -
    -
    {t('addition.common.property')}
    -
    - )} - {edgeTypeStore.newEdgeType.property_indexes.map( - ({ name, type, fields }, index) => ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - edgeTypeStore.newEdgeType.property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType('propertyIndexes'); - } - }} - /> -
    -
    - -
    -
    - -
    - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const propertyIndexEntities = cloneDeep( - edgeTypeStore.newEdgeType.property_indexes - ); - - propertyIndexEntities.splice(index, 1); - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - property_indexes: propertyIndexEntities - }); - - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType( - 'propertyIndexes' - ); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeletePopIndex(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: closeIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeletePopIndex(index); - } - }} - childrenWrapperElement="img" - /> -
    - ) - )} - { - if ( - edgeTypeStore.newEdgeType.property_indexes.length === 0 || - edgeTypeStore.isAddNewPropertyIndexReady - ) { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - property_indexes: [ - ...edgeTypeStore.newEdgeType.property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - edgeTypeStore.validateAllNewEdgeType(true); - // set isAddNewPropertyIndexReady to false - edgeTypeStore.validateNewEdgeType('propertyIndexes', true); - } - }} - style={{ - cursor: 'pointer', - color: edgeTypeStore.isAddNewPropertyIndexReady - ? '#2b65ff' - : '#999', - lineHeight: '32px' - }} - > - {t('addition.edge.add-group')} - -
    -
    - -
    -
    - - -
    -
    - - ); -}); - -export default NewVertexType; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/ReuseEdgeTypes.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/ReuseEdgeTypes.less deleted file mode 100644 index 71698137e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/ReuseEdgeTypes.less +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.reuse-properties-wrapper { - width: 100%; - background: #fff; - margin-top: 16px; - padding: 16px; - - .reuse-steps { - width: 860px; - margin: 16px auto 18px; - } - - .reuse-properties { - &-row { - display: flex; - align-items: center; - margin: 18px 0 32px; - font-size: 14px; - color: #333; - - &:last-child { - margin-bottom: 24px; - } - - &-name { - width: 98px; - text-align: right; - margin-right: 13.9px; - } - } - - &-manipulations { - display: flex; - justify-content: center; - margin-bottom: 88px; - } - - &-validate { - &-duplicate, - &-exist, - &-pass { - width: 58px; - margin: 0 auto; - border-radius: 2px; - letter-spacing: 0; - line-height: 22px; - text-align: center; - } - - &-duplicate { - background: #fff2f2; - border: 1px solid #ff9499; - color: #e64552; - } - - &-exist, - &-pass { - background: #f2fff4; - border: 1px solid #7ed988; - color: #39bf45; - } - } - - &-complete-hint { - display: flex; - flex-direction: column; - justify-content: center; - margin: 88px auto 327px; - - &-description { - display: flex; - justify-content: center; - - & > div { - margin-left: 20px; - } - } - - &-manipulations { - margin: 42px auto 0; - } - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/ReuseEdgeTypes.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/ReuseEdgeTypes.tsx deleted file mode 100644 index 6a462e4ad..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/ReuseEdgeTypes.tsx +++ /dev/null @@ -1,1237 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useState, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { - Select, - Steps, - Transfer, - Button, - Table, - Input, - Message -} from 'hubble-ui'; - -import { GraphManagementStoreContext } from '../../../../stores'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; - -import PassIcon from '../../../../assets/imgs/ic_pass.svg'; - -import './ReuseEdgeTypes.less'; -import { cloneDeep } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; - -const ReuseEdgeTypes: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const metadataConfigsRootStore = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - const { edgeTypeStore } = metadataConfigsRootStore; - const [currentStatus, setCurrentStatus] = useState(1); - // acutally the name, not id in database - const [selectedId, mutateSelectedId] = useState<[] | string>([]); - const [selectedList, mutateSelectedList] = useState([]); - - // step 2 - const [edgeTypeEditIndex, setEdgeTypeEditIndex] = useState( - null - ); - const [vertexTypeEditIndex, setVertexTypeEditIndex] = useState( - null - ); - const [propertyEditIndex, setPropertyEditIndex] = useState( - null - ); - const [propertyIndexEditIndex, setPropertyIndexEditIndex] = useState< - number | null - >(null); - - // hack: need to call @observable at here to dispatch re-render by mobx - // since @action in onBlur() in doesn't dispatch re-render - edgeTypeStore.validateReuseErrorMessage.edgeType.toUpperCase(); - edgeTypeStore.validateReuseErrorMessage.vertexType.toUpperCase(); - edgeTypeStore.validateReuseErrorMessage.property.toUpperCase(); - edgeTypeStore.validateReuseErrorMessage.property_index.toUpperCase(); - - const edgeTypeColumnConfigs = [ - { - title: t('addition.common.edge-type-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (index !== edgeTypeEditIndex) { - return ( -
    - {text} -
    - ); - } - - return ( - { - edgeTypeStore.resetValidateReuseErrorMessage('edgeType'); - - const editedCheckedReusableData = cloneDeep( - edgeTypeStore.editedCheckedReusableData! - ); - - editedCheckedReusableData.edgelabel_conflicts[index].entity.name = - e.value; - - edgeTypeStore.mutateEditedReusableData(editedCheckedReusableData); - }} - originInputProps={{ - onBlur() { - edgeTypeStore.validateReuseData( - 'edgeType', - edgeTypeStore.checkedReusableData!.edgelabel_conflicts[index] - .entity.name, - edgeTypeStore.editedCheckedReusableData!.edgelabel_conflicts[ - index - ].entity.name - ); - } - }} - /> - ); - } - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - width: '30%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if (edgeTypeStore.reusableEdgeTypeNameChangeIndexes.has(index)) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'DEP_CONFLICT': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.dependency-conflict'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - const originalName = edgeTypeStore.checkedReusableData! - .edgelabel_conflicts[index].entity.name; - const changedName = edgeTypeStore.editedCheckedReusableData! - .edgelabel_conflicts[index].entity.name; - const isChanged = changedName !== originalName; - - if (edgeTypeEditIndex === index) { - return ( -
    - { - if ( - !isChanged || - !edgeTypeStore.validateReuseData( - 'edgeType', - originalName, - changedName - ) - ) { - return; - } - - edgeTypeStore.mutateReuseData( - 'edgeType', - originalName, - changedName - ); - setEdgeTypeEditIndex(null); - edgeTypeStore.mutateReusableEdgeTypeChangeIndexes(index); - }} - > - {t('addition.common.save')} - - { - edgeTypeStore.resetValidateReuseErrorMessage('edgeType'); - setEdgeTypeEditIndex(null); - edgeTypeStore.resetEditedReusableEdgeTypeName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (edgeTypeEditIndex !== null) { - return; - } - - setEdgeTypeEditIndex(index); - }} - > - {t('addition.operate.rename')} - - { - if (edgeTypeEditIndex !== null) { - return; - } - - setEdgeTypeEditIndex(null); - - // remove selected status of the property in - const newSelectedList = [...selectedList].filter( - (property) => - property !== - edgeTypeStore.editedCheckedReusableData! - .edgelabel_conflicts[index].entity.name - ); - - mutateSelectedList(newSelectedList); - - // notice: useState hooks cannot sync updated state value, so the length is still 1 - if (selectedList.length === 1) { - setCurrentStatus(1); - // remove edit status after return previous - edgeTypeStore.clearReusableNameChangeIndexes(); - return; - } - - edgeTypeStore.deleteReuseData('edgelabel_conflicts', index); - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - const vertexTypeColumnConfigs = [ - { - title: t('addition.common.vertex-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (index !== vertexTypeEditIndex) { - return text; - } - - return ( - { - edgeTypeStore.resetValidateReuseErrorMessage('vertexType'); - - const editedCheckedReusableData = cloneDeep( - edgeTypeStore.editedCheckedReusableData! - ); - - editedCheckedReusableData.vertexlabel_conflicts[ - index - ].entity.name = e.value; - - edgeTypeStore.mutateEditedReusableData(editedCheckedReusableData); - }} - originInputProps={{ - onBlur() { - edgeTypeStore.validateReuseData( - 'vertexType', - edgeTypeStore.checkedReusableData!.vertexlabel_conflicts[ - index - ].entity.name, - edgeTypeStore.editedCheckedReusableData! - .vertexlabel_conflicts[index].entity.name - ); - } - }} - /> - ); - } - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - width: '30%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if (edgeTypeStore.reusableVertexTypeNameChangeIndexes.has(index)) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'DEP_CONFLICT': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.dependency-conflict'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - const originalName = edgeTypeStore.checkedReusableData! - .vertexlabel_conflicts[index].entity.name; - const changedName = edgeTypeStore.editedCheckedReusableData! - .vertexlabel_conflicts[index].entity.name; - const isChanged = changedName !== originalName; - - if (index === vertexTypeEditIndex) { - return ( -
    - { - if ( - !isChanged || - !edgeTypeStore.validateReuseData( - 'vertexType', - originalName, - changedName - ) - ) { - return; - } - - edgeTypeStore.mutateReuseData( - 'vertexType', - originalName, - changedName - ); - setVertexTypeEditIndex(null); - edgeTypeStore.mutateReusableVertexTypeChangeIndexes(index); - }} - > - {t('addition.common.save')} - - { - edgeTypeStore.resetValidateReuseErrorMessage('vertexType'); - setVertexTypeEditIndex(null); - edgeTypeStore.resetEditedReusableVertexTypeName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (vertexTypeEditIndex !== null) { - return; - } - - setVertexTypeEditIndex(index); - }} - > - {t('addition.operate.rename')} - -
    - ); - } - } - ]; - - const metadataPropertyColumnConfigs = [ - { - title: t('addition.common.property-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (propertyEditIndex !== index) { - return text; - } - - return ( - { - edgeTypeStore.resetValidateReuseErrorMessage('property'); - - const editedCheckedReusableData = cloneDeep( - edgeTypeStore.editedCheckedReusableData! - ); - - editedCheckedReusableData.propertykey_conflicts[ - index - ].entity.name = e.value; - - edgeTypeStore.mutateEditedReusableData(editedCheckedReusableData); - }} - originInputProps={{ - onBlur() { - edgeTypeStore.validateReuseData( - 'property', - edgeTypeStore.checkedReusableData!.propertykey_conflicts[ - index - ].entity.name, - edgeTypeStore.editedCheckedReusableData! - .propertykey_conflicts[index].entity.name - ); - } - }} - /> - ); - } - }, - { - title: t('addition.common.data-type'), - dataIndex: 'data_type', - width: '15%', - render(text: string) { - if (text === 'TEXT') { - return 'string'; - } - - return text.toLowerCase(); - } - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - align: 'center', - width: '15%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if (edgeTypeStore.reusablePropertyNameChangeIndexes.has(index)) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'DEP_CONFLICT': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.dependency-conflict'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return
    {text}
    ; - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - if (index === propertyEditIndex) { - const originalName = edgeTypeStore.checkedReusableData! - .propertykey_conflicts[index].entity.name; - const changedName = edgeTypeStore.editedCheckedReusableData! - .propertykey_conflicts[index].entity.name; - const isChanged = changedName !== originalName; - - return ( -
    - { - if ( - !isChanged || - !edgeTypeStore.validateReuseData( - 'property', - originalName, - changedName - ) - ) { - return; - } - - edgeTypeStore.mutateReuseData( - 'property', - originalName, - changedName - ); - setPropertyEditIndex(null); - edgeTypeStore.mutateReusablePropertyNameChangeIndexes(index); - }} - > - {t('addition.common.save')} - - { - edgeTypeStore.resetValidateReuseErrorMessage('property'); - setPropertyEditIndex(null); - edgeTypeStore.resetEditedReusablePropertyName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (propertyEditIndex !== null) { - return; - } - - setPropertyEditIndex(index); - }} - > - {t('addition.operate.rename')} - - { - if (propertyEditIndex !== null) { - return; - } - - setPropertyEditIndex(null); - - edgeTypeStore.deleteReuseData('propertykey_conflicts', index); - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - const metadataPropertyIndexColumnConfigs = [ - { - title: t('addition.common.property-index-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (index !== propertyIndexEditIndex) { - return text; - } - - return ( - { - edgeTypeStore.resetValidateReuseErrorMessage('property_index'); - - const editedCheckedReusableData = cloneDeep( - edgeTypeStore.editedCheckedReusableData! - ); - - editedCheckedReusableData.propertyindex_conflicts[ - index - ].entity.name = e.value; - - edgeTypeStore.mutateEditedReusableData(editedCheckedReusableData); - }} - originInputProps={{ - onBlur() { - edgeTypeStore.validateReuseData( - 'property_index', - edgeTypeStore.checkedReusableData!.propertyindex_conflicts[ - index - ].entity.name, - edgeTypeStore.editedCheckedReusableData! - .propertyindex_conflicts[index].entity.name - ); - } - }} - /> - ); - } - }, - { - title: t('addition.common.corresponding-type'), - dataIndex: 'owner', - width: '15%' - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - align: 'center', - width: '15%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if (edgeTypeStore.reusablePropertyIndexNameChangeIndexes.has(index)) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'DEP_CONFLICT': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.dependency-conflict'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return
    {text}
    ; - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - if (index === propertyIndexEditIndex) { - const originalName = edgeTypeStore.checkedReusableData! - .propertyindex_conflicts[index].entity.name; - const changedName = edgeTypeStore.editedCheckedReusableData! - .propertyindex_conflicts[index].entity.name; - const isChanged = changedName !== originalName; - - return ( -
    - { - if ( - !isChanged || - !edgeTypeStore.validateReuseData( - 'property_index', - originalName, - changedName - ) - ) { - return; - } - - edgeTypeStore.mutateReuseData( - 'property_index', - originalName, - changedName - ); - setPropertyIndexEditIndex(null); - edgeTypeStore.mutateReusableVertexTypeChangeIndexes(index); - }} - > - {t('addition.common.save')} - - { - edgeTypeStore.resetValidateReuseErrorMessage( - 'property_index' - ); - setPropertyIndexEditIndex(null); - edgeTypeStore.resetEditedReusablePropertyIndexName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (propertyIndexEditIndex !== null) { - return; - } - - setPropertyIndexEditIndex(index); - }} - > - {t('addition.operate.rename')} - - { - if (propertyIndexEditIndex !== null) { - return; - } - - setPropertyIndexEditIndex(null); - - edgeTypeStore.deleteReuseData('propertyindex_conflicts', index); - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - useEffect(() => { - // unlike metadata properties, all vertex types only needs here(in reuse) - edgeTypeStore.fetchEdgeTypeList({ fetchAll: true }); - }, [edgeTypeStore]); - - return ( -
    -
    - {t('addition.edge.multiplexing-edge-type')} -
    -
    - {t('addition.edge.multiplexing-edge-type-notice')} -
    -
    - - {[ - t('addition.menu.select-reuse-item'), - t('addition.menu.confirm-reuse-item'), - t('addition.menu.complete-reuse') - ].map((title: string, index: number) => ( - index + 1 - ? 'finish' - : 'wait' - } - key={title} - /> - ))} - - - {currentStatus === 1 && ( - <> -
    -
    - * - {t('addition.newGraphConfig.id')}: -
    - -
    -
    -
    - * - {t('addition.edge.multiplexing-edge-type')}: -
    - name - )} - selectedList={selectedList} - showSearchBox={false} - candidateTreeStyle={{ - width: 359, - fontSize: 14 - }} - selectedTreeStyle={{ - width: 359, - fontSize: 14 - }} - handleSelect={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleSelectAll={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleDelete={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleDeleteAll={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - /> -
    -
    - - -
    - - )} - - {currentStatus === 2 && ( - <> -
    - {t('addition.common.selected-edge-type')} -
    -
    ({ - name: entity.name, - status - }) - ) - : [] - } - pagination={false} - /> - -
    - {t('addition.common.selected-vertex-type')} -
    -
    ({ - name: entity.name, - status - }) - ) - : [] - } - pagination={false} - /> - -
    - {t('addition.common.selected-property')} -
    -
    ({ - name: entity.name, - data_type: entity.data_type, - status - }) - ) - : [] - } - pagination={false} - /> - -
    - {t('addition.common.selected-property-index')} -
    -
    ({ - name: entity.name, - owner: entity.owner, - status - }) - ) - : [] - } - pagination={false} - /> - -
    - - -
    - - )} - - {currentStatus === 3 && ( -
    -
    - {t('addition.message.reuse-complete')} -
    -
    {t('addition.message.reuse-complete')}
    -
    {t('addition.message.vertex-type-reuse-success')}
    -
    -
    -
    - - -
    -
    - )} - - - ); -}); - -export default ReuseEdgeTypes; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/index.ts deleted file mode 100644 index 2326af6d4..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/edge-type/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import NewEdgeType from './NewEdgeType'; -import EdgeTypeList from './EdgeTypeList'; - -export { NewEdgeType, EdgeTypeList }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckAndEditEdge.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckAndEditEdge.tsx deleted file mode 100644 index 2ff3edae7..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckAndEditEdge.tsx +++ /dev/null @@ -1,1415 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { cloneDeep, merge, isUndefined, isEmpty } from 'lodash-es'; -import { - Drawer, - Button, - Input, - Select, - Switch, - Checkbox, - Message -} from 'hubble-ui'; - -import { Tooltip } from '../../../common'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import { - mapMetadataProperties, - generateGraphModeId, - formatVertexIdText, - edgeWidthMapping -} from '../../../../stores/utils'; - -import type { EdgeTypeValidatePropertyIndexes } from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import CloseIcon from '../../../../assets/imgs/ic_close_16.svg'; -import SelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow_selected.svg'; -import NoSelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow.svg'; -import SelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight_selected.svg'; -import NoSelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight.svg'; -import i18next from '../../../../i18n'; -import { useTranslation } from 'react-i18next'; - -const propertyIndexTypeMappings: Record = { - SECONDARY: i18next.t('addition.menu.secondary-index'), - RANGE: i18next.t('addition.menu.range-index'), - SEARCH: i18next.t('addition.menu.full-text-index') -}; - -const CheckAndEditEdge: React.FC = observer(() => { - const { metadataPropertyStore, edgeTypeStore, graphViewStore } = useContext( - MetadataConfigsRootStore - ); - const { t } = useTranslation(); - const [isAddProperty, switchIsAddProperty] = useState(false); - const [isDeletePop, switchDeletePop] = useState(false); - const [ - deleteExistPopIndexInDrawer, - setDeleteExistPopIndexInDrawer - ] = useState(null); - const [ - deleteAddedPopIndexInDrawer, - setDeleteAddedPopIndexInDrawer - ] = useState(null); - - const deleteWrapperRef = useRef(null); - const dropdownWrapperRef = useRef(null); - const deleteWrapperInDrawerRef = useRef(null); - - const isEditEdge = graphViewStore.currentDrawer === 'edit-edge'; - - const metadataDrawerOptionClass = classnames({ - 'metadata-drawer-options': true, - 'metadata-drawer-options-disabled': isEditEdge - }); - - // need useCallback to stop infinite callings of useEffect - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - const drawerWrapper = document.querySelector( - '.new-fc-one-drawer-content-wrapper' - ); - - if ( - graphViewStore.currentDrawer === 'check-edge' && - drawerWrapper && - !drawerWrapper.contains(e.target as Element) - ) { - /* - handleOutSideClick is being called after the value assignment of data and drawer-name, - we need to judge whether a node or edge is being clicked - */ - if (graphViewStore.isNodeOrEdgeClicked) { - // if node/edge is clicked, reset state and prepare for next outside clicks - graphViewStore.switchNodeOrEdgeClicked(false); - } else { - graphViewStore.setCurrentDrawer(''); - } - } - - if ( - isEditEdge && - isAddProperty && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddProperty(false); - return; - } - - if ( - isDeletePop !== null && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchDeletePop(false); - } - }, - [graphViewStore, isEditEdge, isAddProperty, isDeletePop] - ); - - const handleCloseDrawer = () => { - switchIsAddProperty(false); - graphViewStore.setCurrentDrawer(''); - edgeTypeStore.selectEdgeType(null); - // clear mutations in - edgeTypeStore.resetEditedSelectedEdgeType(); - }; - - const handleDeleteEdge = async () => { - // cache vertex name here before it gets removed - const edgeName = edgeTypeStore.selectedEdgeType!.name; - const edgeId = generateGraphModeId( - edgeName, - edgeTypeStore.selectedEdgeType!.source_label, - edgeTypeStore.selectedEdgeType!.target_label - ); - const edgeInfo = graphViewStore.visDataSet!.edges.get(edgeName); - - switchDeletePop(false); - handleCloseDrawer(); - - graphViewStore.visDataSet!.edges.remove(edgeId); - - await edgeTypeStore.deleteEdgeType([edgeName]); - - if (edgeTypeStore.requestStatus.deleteEdgeType === 'success') { - Message.success({ - content: t('addition.common.del-success'), - size: 'medium', - showCloseIcon: false - }); - - edgeTypeStore.fetchEdgeTypeList({ fetchAll: true }); - } - - if (edgeTypeStore.requestStatus.deleteEdgeType === 'failed') { - Message.error({ - content: edgeTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - - // if failed, re-add edge - graphViewStore.visDataSet?.edges.add(edgeInfo); - } - }; - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - if (edgeTypeStore.selectedEdgeType === null) { - return null; - } - - return ( - { - if (!isEditEdge) { - graphViewStore.setCurrentDrawer('edit-edge'); - edgeTypeStore.validateEditEdgeType(); - } else { - const id = generateGraphModeId( - edgeTypeStore.selectedEdgeType!.name, - edgeTypeStore.selectedEdgeType!.source_label, - edgeTypeStore.selectedEdgeType!.target_label - ); - const updateInfo: Record = {}; - - if ( - !isEmpty(edgeTypeStore.editedSelectedEdgeType.append_properties) - ) { - const mappedProperties = mapMetadataProperties( - edgeTypeStore.selectedEdgeType!.properties, - metadataPropertyStore.metadataProperties - ); - - const newMappedProperties = mapMetadataProperties( - edgeTypeStore.editedSelectedEdgeType.append_properties, - metadataPropertyStore.metadataProperties - ); - - const mergedProperties = merge( - mappedProperties, - newMappedProperties - ); - - updateInfo.title = ` - - - ${Object.entries(mergedProperties) - .map(([key, value]) => { - const convertedValue = - value.toLowerCase() === 'text' - ? 'string' - : value.toLowerCase(); - - return ``; - }) - .join('')} - `; - } - - if (edgeTypeStore.editedSelectedEdgeType.style.color !== null) { - updateInfo.color = { - color: edgeTypeStore.editedSelectedEdgeType.style.color, - highlight: edgeTypeStore.editedSelectedEdgeType.style.color, - hover: edgeTypeStore.editedSelectedEdgeType.style.color - }; - } - - if ( - edgeTypeStore.editedSelectedEdgeType.style.with_arrow !== null - ) { - updateInfo.arrows = - edgeTypeStore.editedSelectedEdgeType.style.with_arrow === true - ? 'to' - : ''; - } - - if ( - edgeTypeStore.editedSelectedEdgeType.style.thickness !== null - ) { - updateInfo.value = - edgeWidthMapping[ - edgeTypeStore.editedSelectedEdgeType.style.thickness - ]; - } - - if (!isEmpty(updateInfo)) { - updateInfo.id = id; - - graphViewStore.visDataSet!.edges.update(updateInfo); - } - await edgeTypeStore.updateEdgeType(); - - if (edgeTypeStore.requestStatus.updateEdgeType === 'failed') { - Message.error({ - content: edgeTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - - return; - } - - if (edgeTypeStore.requestStatus.updateEdgeType === 'success') { - Message.success({ - content: t('addition.operate.modify-success'), - size: 'medium', - showCloseIcon: false - }); - } - - handleCloseDrawer(); - graphViewStore.visNetwork!.unselectAll(); - edgeTypeStore.fetchEdgeTypeList({ fetchAll: true }); - } - }} - key="drawer-manipulation" - > - {isEditEdge ? t('addition.common.save') : t('addition.common.edit')} - , - -

    - {t('addition.edge.confirm-del-edge-type')} -

    -

    {t('addition.edge.confirm-del-edge-careful-notice')}

    -

    {t('addition.message.long-time-notice')}

    -
    -
    - {t('addition.common.confirm')} -
    -
    { - switchDeletePop(false); - }} - > - {t('addition.common.cancel')} -
    -
    - - } - childrenProps={{ - onClick() { - if (isEditEdge) { - handleCloseDrawer(); - return; - } - - switchDeletePop(true); - } - }} - > - -
    - ]} - > -
    -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - {t('addition.common.edge-type-name')}: -
    -
    - {edgeTypeStore.selectedEdgeType!.name} -
    -
    - -
    -
    - - {t('addition.common.edge-style')}: - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - {t('addition.common.source-type')}: -
    -
    - {edgeTypeStore.selectedEdgeType!.source_label} -
    -
    -
    -
    - {t('addition.common.target-type')}: -
    -
    - {edgeTypeStore.selectedEdgeType!.target_label} -
    -
    -
    -
    - {t('addition.common.allow-multiple-connections')}: -
    - -
    -
    -
    - {t('addition.common.association-property')}: -
    -
    -
    - {t('addition.common.property')} - {t('addition.common.allow-null')} -
    - {edgeTypeStore.selectedEdgeType!.properties.map( - ({ name, nullable }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditEdge && - edgeTypeStore.editedSelectedEdgeType.append_properties.map( - ({ name }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditEdge && ( -
    { - switchIsAddProperty(!isAddProperty); - }} - > - - {t('addition.common.add-property')} - - toogleAddProperties -
    - )} - {isEditEdge && isAddProperty && ( -
    - {metadataPropertyStore.metadataProperties - .filter( - (property) => - edgeTypeStore.selectedEdgeType!.properties.find( - ({ name }) => name === property.name - ) === undefined - ) - .map((property) => ( -
    - - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesInSelectedVertextType = - edgeTypeStore.addedPropertiesInSelectedEdgeType; - - addedPropertiesInSelectedVertextType.has( - property.name - ) - ? addedPropertiesInSelectedVertextType.delete( - property.name - ) - : addedPropertiesInSelectedVertextType.add( - property.name - ); - - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - append_properties: [ - ...addedPropertiesInSelectedVertextType - ].map((propertyName) => { - const currentProperty = edgeTypeStore.newEdgeType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined(currentProperty) - ? currentProperty.nullable - : true - }; - }) - }); - }} - > - {property.name} - - -
    - ))} -
    - )} -
    -
    - -
    -
    - {t('addition.common.distinguishing-key-property')}: -
    -
    - {edgeTypeStore.selectedEdgeType!.sort_keys.join(';')} -
    -
    - -
    -
    - - {t('addition.edge.display-content')}: - -
    - {isEditEdge ? ( - - ) : ( -
    - {edgeTypeStore.selectedEdgeType?.style.display_fields - .map((field) => - formatVertexIdText( - field, - t('addition.function-parameter.edge-type') - ) - ) - .join('-')} -
    - )} -
    - -
    - {t('addition.edge.index-info')} -
    -
    -
    - {t('addition.menu.type-index')}: -
    - -
    -
    -
    - {t('addition.common.property-index')}: -
    -
    - {(edgeTypeStore.selectedEdgeType!.property_indexes.length !== 0 || - edgeTypeStore.editedSelectedEdgeType.append_property_indexes - .length !== 0) && ( -
    - {t('addition.edge.index-name')} - {t('addition.edge.index-type')} - {t('addition.common.property')} -
    - )} - {edgeTypeStore - .selectedEdgeType!.property_indexes.filter((propertyIndex) => - isUndefined( - edgeTypeStore.editedSelectedEdgeType.remove_property_indexes.find( - (removedPropertyName) => - removedPropertyName === propertyIndex.name - ) - ) - ) - .map(({ name, type, fields }, index) => { - return ( -
    -
    {name}
    -
    {propertyIndexTypeMappings[type]}
    -
    - - {fields - .map((field, index) => index + 1 + '.' + field) - .join(';')} - - - {isEditEdge && ( - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const removedPropertyIndex = cloneDeep( - edgeTypeStore.editedSelectedEdgeType - .remove_property_indexes - ); - - removedPropertyIndex.push( - edgeTypeStore.selectedEdgeType! - .property_indexes[index].name - ); - - edgeTypeStore.mutateEditedSelectedEdgeType( - { - ...edgeTypeStore.editedSelectedEdgeType, - remove_property_indexes: removedPropertyIndex - } - ); - - setDeleteExistPopIndexInDrawer(null); - edgeTypeStore.validateEditEdgeType(true); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeleteExistPopIndexInDrawer(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteExistPopIndexInDrawer(index); - } - }} - childrenWrapperElement="img" - /> - )} -
    -
    - ); - })} - {edgeTypeStore.editedSelectedEdgeType.append_property_indexes.map( - ({ name, type, fields }, index) => { - return ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - edgeTypeStore.editedSelectedEdgeType - .append_property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - append_property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - edgeTypeStore.validateEditEdgeType(); - } - }} - /> -
    -
    - -
    -
    - - - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const appendPropertyIndexes = cloneDeep( - edgeTypeStore.editedSelectedEdgeType! - .append_property_indexes - ); - - appendPropertyIndexes.splice(index, 1); - - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - append_property_indexes: appendPropertyIndexes - }); - - setDeleteAddedPopIndexInDrawer(null); - edgeTypeStore.validateEditEdgeType(true); - }} - > - {t('addition.common.confirm')} -
    -
    { - edgeTypeStore.resetEditedSelectedEdgeType(); - setDeleteAddedPopIndexInDrawer(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteAddedPopIndexInDrawer(index); - } - }} - childrenWrapperElement="img" - /> -
    -
    - ); - } - )} - {isEditEdge && ( -
    { - if ( - edgeTypeStore.editedSelectedEdgeType - .append_property_indexes.length === 0 || - edgeTypeStore.isEditReady - ) { - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - append_property_indexes: [ - ...edgeTypeStore.editedSelectedEdgeType - .append_property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - edgeTypeStore.validateEditEdgeType(true); - } - }} - style={{ - cursor: 'pointer', - color: edgeTypeStore.isEditReady ? '#2b65ff' : '#999' - }} - > - {t('addition.edge.add-group')} -
    - )} -
    -
    - - -
    - ); -}); - -export default CheckAndEditEdge; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckAndEditVertex.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckAndEditVertex.tsx deleted file mode 100644 index c8a064209..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckAndEditVertex.tsx +++ /dev/null @@ -1,1310 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { cloneDeep, isUndefined, merge, isEmpty } from 'lodash-es'; -import { - Drawer, - Button, - Input, - Select, - Switch, - Checkbox, - Message -} from 'hubble-ui'; - -import { Tooltip } from '../../../common'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import { - mapMetadataProperties, - formatVertexIdText, - vertexRadiusMapping -} from '../../../../stores/utils'; - -import type { - VertexTypeValidatePropertyIndexes, - VertexTypeProperty -} from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import CloseIcon from '../../../../assets/imgs/ic_close_16.svg'; -import i18next from '../../../../i18n'; -import { useTranslation } from 'react-i18next'; - -const IDStrategyMappings: Record = { - PRIMARY_KEY: i18next.t('addition.constant.primary-key-id'), - AUTOMATIC: i18next.t('addition.constant.automatic-generation'), - CUSTOMIZE_STRING: i18next.t('addition.constant.custom-string'), - CUSTOMIZE_NUMBER: i18next.t('addition.constant.custom-number'), - CUSTOMIZE_UUID: i18next.t('addition.constant.custom-uuid') -}; - -const propertyIndexTypeMappings: Record = { - SECONDARY: i18next.t('addition.menu.secondary-index'), - RANGE: i18next.t('addition.menu.range-index'), - SEARCH: i18next.t('addition.menu.full-text-index') -}; - -const CheckAndEditVertex: React.FC = observer(() => { - const { metadataPropertyStore, vertexTypeStore, graphViewStore } = useContext( - MetadataConfigsRootStore - ); - const { t } = useTranslation(); - const [isAddProperty, switchIsAddProperty] = useState(false); - const [isDeletePop, switchDeletePop] = useState(false); - const [ - deleteExistPopIndexInDrawer, - setDeleteExistPopIndexInDrawer - ] = useState(null); - const [ - deleteAddedPopIndexInDrawer, - setDeleteAddedPopIndexInDrawer - ] = useState(null); - - const deleteWrapperRef = useRef(null); - const dropdownWrapperRef = useRef(null); - const deleteWrapperInDrawerRef = useRef(null); - - const isEditVertex = graphViewStore.currentDrawer === 'edit-vertex'; - - const metadataDrawerOptionClass = classnames({ - 'metadata-drawer-options': true, - 'metadata-drawer-options-disabled': isEditVertex - }); - - // need useCallback to stop infinite callings of useEffect - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - const drawerWrapper = document.querySelector( - '.new-fc-one-drawer-content-wrapper' - ); - - if ( - graphViewStore.currentDrawer === 'check-vertex' && - drawerWrapper && - !drawerWrapper.contains(e.target as Element) - ) { - /* - handleOutSideClick is being called after the value assignment of data and drawer-name, - we need to judge whether a node or edge is being clicked - */ - if (graphViewStore.isNodeOrEdgeClicked) { - // if node/edge is clicked, reset state and prepare for next outside clicks - graphViewStore.switchNodeOrEdgeClicked(false); - } else { - graphViewStore.setCurrentDrawer(''); - } - } - - if ( - isAddProperty && - isEditVertex && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddProperty(false); - return; - } - - if ( - isDeletePop && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchDeletePop(false); - } - }, - [graphViewStore, isAddProperty, isEditVertex, isDeletePop] - ); - - const handleCloseDrawer = () => { - switchIsAddProperty(false); - graphViewStore.setCurrentDrawer(''); - vertexTypeStore.selectVertexType(null); - // clear mutations in - vertexTypeStore.resetEditedSelectedVertexType(); - }; - - const handleDeleteVertex = async () => { - // cache vertex name here before it gets removed - const vertexName = vertexTypeStore.selectedVertexType!.name; - const vertexInfo = graphViewStore.visDataSet?.nodes.get(vertexName); - const connectedEdgeInfos = graphViewStore.visDataSet?.edges.get( - graphViewStore.visNetwork?.getConnectedEdges(vertexName) - ); - - // close - handleCloseDrawer(); - switchDeletePop(false); - - // if node > 1, delete node on local before send request - if (graphViewStore.visDataSet!.nodes.length > 1) { - graphViewStore.visDataSet!.nodes.remove(vertexName); - } - - await vertexTypeStore.deleteVertexType([vertexName]); - - if (vertexTypeStore.requestStatus.deleteVertexType === 'success') { - Message.success({ - content: t('addition.common.del-success'), - size: 'medium', - showCloseIcon: false - }); - - vertexTypeStore.fetchVertexTypeList({ fetchAll: true }); - - // if delete the last node, fetch graph data to trigger re-render to reveal - if (graphViewStore.visDataSet?.nodes.length === 1) { - graphViewStore.switchGraphDataEmpty(true); - graphViewStore.fetchGraphViewData(); - } - } - - if (vertexTypeStore.requestStatus.deleteVertexType === 'failed') { - Message.error({ - content: vertexTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - - // if failed, re-add vertex and edges - graphViewStore.visDataSet!.nodes.add(vertexInfo); - graphViewStore.visDataSet!.edges.add(connectedEdgeInfos); - } - }; - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - if (vertexTypeStore.selectedVertexType === null) { - return null; - } - - return ( - { - if (!isEditVertex) { - graphViewStore.setCurrentDrawer('edit-vertex'); - vertexTypeStore.validateEditVertexType(); - } else { - const id = vertexTypeStore.selectedVertexType!.name; - const updateInfo: Record = {}; - - if ( - !isEmpty( - vertexTypeStore.editedSelectedVertexType.append_properties - ) - ) { - const mappedProperties = mapMetadataProperties( - vertexTypeStore.selectedVertexType!.properties, - metadataPropertyStore.metadataProperties - ); - - const newMappedProperties = mapMetadataProperties( - vertexTypeStore.editedSelectedVertexType.append_properties, - metadataPropertyStore.metadataProperties - ); - - const mergedProperties = merge( - mappedProperties, - newMappedProperties - ); - - updateInfo.title = ` - - - ${Object.entries(mergedProperties) - .map(([key, value]) => { - const convertedValue = - value.toLowerCase() === 'text' - ? 'string' - : value.toLowerCase(); - - return ``; - }) - .join('')} - `; - } - - if ( - vertexTypeStore.editedSelectedVertexType.style.color !== null - ) { - updateInfo.color = { - background: - vertexTypeStore.editedSelectedVertexType.style.color, - border: vertexTypeStore.editedSelectedVertexType.style.color - }; - } - - if ( - vertexTypeStore.editedSelectedVertexType.style.size !== null - ) { - updateInfo.value = - vertexRadiusMapping[ - vertexTypeStore.editedSelectedVertexType.style.size - ]; - } - - if (!isEmpty(updateInfo)) { - updateInfo.id = id; - - graphViewStore.visDataSet!.nodes.update(updateInfo); - } - await vertexTypeStore.updateVertexType(); - - if (vertexTypeStore.requestStatus.updateVertexType === 'failed') { - Message.error({ - content: vertexTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - - return; - } - - if ( - vertexTypeStore.requestStatus.updateVertexType === 'success' - ) { - Message.success({ - content: t('addition.operate.modify-success'), - size: 'medium', - showCloseIcon: false - }); - } - - handleCloseDrawer(); - graphViewStore.visNetwork!.unselectAll(); - vertexTypeStore.fetchVertexTypeList({ fetchAll: true }); - } - }} - key="drawer-manipulation" - > - {isEditVertex ? t('addition.common.save') : t('addition.common.edit')} - , - - {vertexTypeStore.vertexTypeUsingStatus && - vertexTypeStore.vertexTypeUsingStatus[ - vertexTypeStore.selectedVertexType!.name - ] ? ( -

    - {t('addition.vertex.using-cannot-delete')} -

    - ) : ( - <> -

    - {t('addition.vertex.del-vertex-confirm')} -

    -

    {t('addition.edge.confirm-del-edge-careful-notice')}

    -

    {t('addition.message.long-time-notice')}

    -
    -
    - {t('addition.common.confirm')} -
    -
    { - switchDeletePop(false); - }} - > - {t('addition.common.cancel')} -
    -
    - - )} - - } - childrenProps={{ - onClick() { - if (isEditVertex) { - handleCloseDrawer(); - return; - } - - switchDeletePop(true); - } - }} - > - -
    - ]} - > -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - {t('addition.vertex.vertex-type-name')}: -
    -
    - {vertexTypeStore.selectedVertexType!.name} -
    -
    - -
    -
    - - {t('addition.vertex.vertex-style')}: - -
    -
    - -
    -
    - -
    -
    -
    -
    - {t('addition.common.id-strategy')}: -
    - {IDStrategyMappings[vertexTypeStore.selectedVertexType!.id_strategy]} -
    -
    -
    - {t('addition.common.association-property')}: -
    -
    -
    - {t('addition.common.property')} - {t('addition.common.allow-null')} -
    - {vertexTypeStore.selectedVertexType!.properties.map( - ({ name, nullable }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditVertex && - vertexTypeStore.editedSelectedVertexType.append_properties.map( - ({ name }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditVertex && ( -
    { - switchIsAddProperty(!isAddProperty); - }} - > - - {t('addition.common.add-property')} - - toogleAddProperties -
    - )} - {isEditVertex && isAddProperty && ( -
    - {metadataPropertyStore.metadataProperties - .filter( - (property) => - vertexTypeStore.selectedVertexType!.properties.find( - ({ name }) => name === property.name - ) === undefined - ) - .map((property) => ( -
    - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesInSelectedVertextType = - vertexTypeStore.addedPropertiesInSelectedVertextType; - - addedPropertiesInSelectedVertextType.has( - property.name - ) - ? addedPropertiesInSelectedVertextType.delete( - property.name - ) - : addedPropertiesInSelectedVertextType.add( - property.name - ); - - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - append_properties: [ - ...addedPropertiesInSelectedVertextType - ].map((propertyName) => { - const currentProperty = vertexTypeStore.newVertexType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined(currentProperty) - ? currentProperty.nullable - : true - }; - }) - }); - }} - > - {property.name} - - -
    - ))} -
    - )} -
    -
    -
    -
    - {t('addition.common.primary-key-property')}: -
    -
    - {vertexTypeStore.selectedVertexType!.primary_keys.join(';')} -
    -
    -
    -
    - - {t('addition.vertex.vertex-display-content')}: - -
    - {isEditVertex ? ( - - ) : ( -
    - {vertexTypeStore.selectedVertexType?.style.display_fields - .map((field) => - formatVertexIdText( - field, - t('addition.function-parameter.vertex-id') - ) - ) - .join('-')} -
    - )} -
    -
    - {t('addition.edge.index-info')} -
    -
    -
    - {t('addition.menu.type-index')}: -
    - -
    -
    -
    - {t('addition.common.property-index')}: -
    -
    - {(vertexTypeStore.selectedVertexType!.property_indexes.length !== - 0 || - vertexTypeStore.editedSelectedVertexType.append_property_indexes - .length !== 0) && ( -
    - {t('addition.edge.index-name')} - {t('addition.edge.index-type')} - {t('addition.common.property')} -
    - )} - {vertexTypeStore - .selectedVertexType!.property_indexes.filter((propertyIndex) => - isUndefined( - vertexTypeStore.editedSelectedVertexType.remove_property_indexes.find( - (removedPropertyName) => - removedPropertyName === propertyIndex.name - ) - ) - ) - .map(({ name, type, fields }, index) => { - return ( -
    -
    {name}
    -
    {propertyIndexTypeMappings[type]}
    -
    - - {fields - .map((field, index) => index + 1 + '.' + field) - .join(';')} - - {isEditVertex && ( - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const removedPropertyIndexes = cloneDeep( - vertexTypeStore.editedSelectedVertexType - .remove_property_indexes - ); - - removedPropertyIndexes.push( - vertexTypeStore.selectedVertexType! - .property_indexes[index].name - ); - - vertexTypeStore.mutateEditedSelectedVertexType( - { - ...vertexTypeStore.editedSelectedVertexType, - remove_property_indexes: removedPropertyIndexes - } - ); - - setDeleteExistPopIndexInDrawer(null); - vertexTypeStore.validateEditVertexType( - true - ); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeleteExistPopIndexInDrawer(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteExistPopIndexInDrawer(index); - } - }} - /> - )} -
    -
    - ); - })} - {vertexTypeStore.editedSelectedVertexType.append_property_indexes.map( - ({ name, type, fields }, index) => { - return ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - vertexTypeStore.editedSelectedVertexType - .append_property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - append_property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - vertexTypeStore.validateEditVertexType(); - } - }} - /> -
    -
    - -
    -
    - - - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const appendPropertyIndexes = cloneDeep( - vertexTypeStore.editedSelectedVertexType! - .append_property_indexes - ); - - appendPropertyIndexes.splice(index, 1); - - vertexTypeStore.mutateEditedSelectedVertexType( - { - ...vertexTypeStore.editedSelectedVertexType, - append_property_indexes: appendPropertyIndexes - } - ); - - setDeleteAddedPopIndexInDrawer(null); - vertexTypeStore.validateEditVertexType(true); - }} - > - {t('addition.common.confirm')} -
    -
    { - vertexTypeStore.resetEditedSelectedVertexType(); - setDeleteAddedPopIndexInDrawer(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteAddedPopIndexInDrawer(index); - } - }} - childrenWrapperElement="img" - /> -
    -
    - ); - } - )} - {isEditVertex && ( -
    { - if ( - vertexTypeStore.editedSelectedVertexType - .append_property_indexes.length === 0 || - vertexTypeStore.isEditReady - ) { - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - append_property_indexes: [ - ...vertexTypeStore.editedSelectedVertexType - .append_property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - vertexTypeStore.validateEditVertexType(true); - } - }} - style={{ - cursor: 'pointer', - color: vertexTypeStore.isEditReady ? '#2b65ff' : '#999' - }} - > - {t('addition.edge.add-group')} -
    - )} -
    - - -
    - ); -}); - -export default CheckAndEditVertex; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckProperty.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckProperty.tsx deleted file mode 100644 index 0bcef5713..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CheckProperty.tsx +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import { Drawer, Table, Message } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { Tooltip } from '../../../common'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; - -import type { MetadataProperty } from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; -import { signal } from 'codemirror'; - -const CheckProperty: React.FC = observer(() => { - const { metadataPropertyStore, graphViewStore } = useContext( - MetadataConfigsRootStore - ); - const { t } = useTranslation(); - const [popIndex, setPopIndex] = useState(null); - const deleteWrapperRef = useRef(null); - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - // note: .new-fc-one-drawer-content-wrapper sometimes only contain one single element? - // however if you capture .new-fc-one-drawer-wrapper-body-container it still returns itself and contains all children - // thus here we capture body-container as drawer - const drawerWrapper = document.querySelector( - '.new-fc-one-drawer-wrapper-body-container' - ); - - const deleteWrapper = document.querySelector('.metadata-graph-tooltips'); - - if ( - graphViewStore.currentDrawer === 'check-property' && - drawerWrapper && - !drawerWrapper.contains(e.target as Element) - ) { - if ( - deleteWrapper === null && - (e.target as Element).className !== - 'metadata-graph-property-manipulation' - ) { - graphViewStore.setCurrentDrawer(''); - } - - if ( - deleteWrapper && - !deleteWrapper.contains(e.target as Element) && - (e.target as Element).className !== - 'metadata-graph-property-manipulation' - ) { - graphViewStore.setCurrentDrawer(''); - } - } - - if ( - popIndex !== null && - deleteWrapper && - !deleteWrapper.contains(e.target as Element) - ) { - setPopIndex(null); - } - }, - [graphViewStore, popIndex] - ); - - const handleCloseDrawer = () => { - graphViewStore.setCurrentDrawer(''); - }; - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, true); - - return () => { - document.removeEventListener('click', handleOutSideClick, true); - }; - }, [handleOutSideClick]); - - const columnConfigs = [ - { - title: t('addition.common.property-name'), - dataIndex: 'name' - }, - { - title: t('addition.common.data-type'), - dataIndex: 'data_type', - render(text: string) { - const realText = text === 'TEXT' ? 'string' : text.toLowerCase(); - - return realText; - } - }, - { - title: t('addition.common.cardinal-number'), - dataIndex: 'cardinality' - }, - { - title: t('addition.operate.operate'), - render(_: any, records: MetadataProperty, index: number) { - return ( - - {metadataPropertyStore.metadataPropertyUsingStatus && - metadataPropertyStore.metadataPropertyUsingStatus[ - records.name - ] ? ( -

    - {t('addition.message.property-using-cannot-delete')} -

    - ) : ( - <> -

    {t('addition.message.property-del-confirm')}

    -

    {t('addition.edge.confirm-del-edge-careful-notice')}

    -
    -
    { - setPopIndex(null); - await metadataPropertyStore.deleteMetadataProperty([ - records.name - ]); - if ( - metadataPropertyStore.requestStatus - .deleteMetadataProperty === 'success' - ) { - Message.success({ - content: t('addition.common.del-success'), - size: 'medium', - showCloseIcon: false - }); - metadataPropertyStore.fetchMetadataPropertyList(); - } - if ( - metadataPropertyStore.requestStatus - .deleteMetadataProperty === 'failed' - ) { - Message.error({ - content: metadataPropertyStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('addition.common.confirm')} -
    -
    { - setPopIndex(null); - }} - > - {t('addition.common.cancel')} -
    -
    - - )} - - } - childrenProps={{ - className: 'metadata-graph-property-manipulation', - async onClick() { - await metadataPropertyStore.checkIfUsing([records.name]); - if ( - metadataPropertyStore.requestStatus.checkIfUsing === 'success' - ) { - setPopIndex(index); - } - } - }} - > - {t('addition.common.del')} -
    - ); - } - } - ]; - - return ( - -
    -
    - - - ); -}); - -export default CheckProperty; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateEdge.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateEdge.tsx deleted file mode 100644 index 87268d57b..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateEdge.tsx +++ /dev/null @@ -1,1177 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { cloneDeep, isUndefined } from 'lodash-es'; -import { - Drawer, - Button, - Input, - Select, - Switch, - Checkbox, - Tooltip, - Message -} from 'hubble-ui'; - -import { Tooltip as CustomTooltip } from '../../../common'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import { EdgeTypeValidatePropertyIndexes } from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; -import { - mapMetadataProperties, - generateGraphModeId, - edgeWidthMapping, - formatVertexIdText -} from '../../../../stores/utils'; -import { useTranslation } from 'react-i18next'; - -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import HintIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import closeIcon from '../../../../assets/imgs/ic_close_16.svg'; -import SelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow_selected.svg'; -import NoSelectedSoilidArrowIcon from '../../../../assets/imgs/ic_arrow.svg'; -import SelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight_selected.svg'; -import NoSelectedSoilidStraightIcon from '../../../../assets/imgs/ic_straight.svg'; - -const CreateEdge: React.FC = observer(() => { - const { - metadataPropertyStore, - vertexTypeStore, - edgeTypeStore, - graphViewStore - } = useContext(MetadataConfigsRootStore); - const [isAddNewProperty, switchIsAddNewProperty] = useState(false); - const [deletePopIndex, setDeletePopIndex] = useState(null); - const deleteWrapperRef = useRef(null); - const dropdownWrapperRef = useRef(null); - const { t } = useTranslation(); - // need useCallback to stop infinite callings of useEffect - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - // if clicked element is not on dropdown, collpase it - if ( - isAddNewProperty && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddNewProperty(false); - } - - if ( - deletePopIndex && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - setDeletePopIndex(null); - } - }, - [deletePopIndex, isAddNewProperty] - ); - - const handleCloseDrawer = () => { - graphViewStore.setCurrentDrawer(''); - edgeTypeStore.resetNewEdgeType(); - edgeTypeStore.resetAddedPropertiesInSelectedEdgeType(); - }; - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( - { - edgeTypeStore.validateAllNewEdgeType(); - - if (!edgeTypeStore.isCreatedReady) { - return; - } - - const { - name, - source_label, - target_label, - properties, - sort_keys, - style, - ...rest - } = edgeTypeStore.newEdgeType; - - const mappedProperties = mapMetadataProperties( - properties, - metadataPropertyStore.metadataProperties - ); - - graphViewStore.visDataSet!.edges.add({ - ...rest, - id: generateGraphModeId(name, source_label, target_label), - label: name.length <= 15 ? name : name.slice(0, 15) + '...', - from: source_label, - to: target_label, - value: edgeWidthMapping[style.thickness], - font: { size: 16, strokeWidth: 0, color: '#666' }, - arrows: style.with_arrow === true ? 'to' : '', - title: ` - - - ${Object.entries(mappedProperties) - .map(([key, value]) => { - const convertedValue = - value.toLowerCase() === 'text' - ? 'string' - : value.toLowerCase(); - - const sortKeyIndex = sort_keys.findIndex( - (sortKey) => sortKey === key - ); - - return ``; - }) - .join('')} - `, - color: { - color: style.color, - highlight: style.color, - hover: style.color - } - }); - - await edgeTypeStore.addEdgeType(); - - if (edgeTypeStore.requestStatus.addEdgeType === 'success') { - edgeTypeStore.fetchEdgeTypeList(); - edgeTypeStore.resetNewEdgeType(); - edgeTypeStore.resetAddedPropertiesInSelectedEdgeType(); - graphViewStore.setCurrentDrawer(''); - - Message.success({ - content: t('addition.newGraphConfig.create-scuccess'), - size: 'medium', - showCloseIcon: false - }); - } - - if (edgeTypeStore.requestStatus.addEdgeType === 'failed') { - Message.error({ - content: edgeTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('addition.newGraphConfig.create')} - , - - ]} - > -
    -
    -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - * - {t('addition.common.edge-type-name')}: -
    - { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - name: e.value - }); - }} - originInputProps={{ - onBlur() { - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType('name'); - } - }} - /> -
    -
    -
    - * - {t('addition.common.edge-style')}: -
    -
    - -
    - -
    - -
    - -
    - -
    -
    -
    -
    - * - {t('addition.common.source-type')}: -
    - -
    - -
    -
    - * - {t('addition.common.target-type')}: -
    - -
    - -
    -
    - * - {t('addition.common.allow-multiple-connections')}: - - hint - -
    - { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - link_multi_times: checked - }); - }} - /> -
    - -
    -
    - {edgeTypeStore.newEdgeType.link_multi_times && ( - * - )} - {t('addition.common.association-property')}: -
    -
    - {edgeTypeStore.newEdgeType.properties.length !== 0 && ( -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.allow-null')}
    -
    - {edgeTypeStore.newEdgeType.properties.map( - (property, index) => { - const currentProperties = cloneDeep( - edgeTypeStore.newEdgeType.properties - ); - - return ( -
    -
    {property.name}
    -
    - { - currentProperties[index].nullable = - !currentProperties[index].nullable; - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - properties: currentProperties - }); - }} - size="large" - /> -
    -
    - ); - } - )} -
    - )} -
    { - switchIsAddNewProperty(!isAddNewProperty); - }} - > - {t('addition.common.add-property')} - toggleAddProperty -
    -
    -
    - - {isAddNewProperty && ( -
    -
    -
    - {metadataPropertyStore.metadataProperties.map((property) => ( -
    - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesIndexInSelectedEdgeType = - edgeTypeStore.addedPropertiesInSelectedEdgeType; - - addedPropertiesIndexInSelectedEdgeType.has( - property.name - ) - ? addedPropertiesIndexInSelectedEdgeType.delete( - property.name - ) - : addedPropertiesIndexInSelectedEdgeType.add( - property.name - ); - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - properties: [ - ...addedPropertiesIndexInSelectedEdgeType - ].map((propertyName) => { - const currentProperty = - edgeTypeStore.newEdgeType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined(currentProperty) - ? currentProperty.nullable - : true - }; - }) - }); - - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType('properties'); - }} - > - {property.name} - - -
    - ))} -
    -
    - )} - - {edgeTypeStore.newEdgeType.link_multi_times && ( -
    -
    - * - {t('addition.common.distinguishing-key')}: -
    - -
    - )} -
    -
    - * - {t('addition.edge.display-content')}: -
    - -
    -
    - - {t('addition.edge.index-info')} - - - hint - -
    - -
    -
    - * - {t('addition.menu.type-index')}: -
    - { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - open_label_index: checked - }); - }} - /> -
    - -
    -
    - {t('addition.common.property-index')}: -
    -
    - {edgeTypeStore.newEdgeType.property_indexes.length !== 0 && ( -
    -
    - {t('addition.edge.index-name')} -
    -
    - {t('addition.edge.index-type')} -
    -
    {t('addition.common.property')}
    -
    - )} - {edgeTypeStore.newEdgeType.property_indexes.map( - ({ name, type, fields }, index) => ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - edgeTypeStore.newEdgeType.property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType( - 'propertyIndexes' - ); - } - }} - /> -
    -
    - -
    -
    - -
    - - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const propertyIndexEntities = cloneDeep( - edgeTypeStore.newEdgeType.property_indexes - ); - - propertyIndexEntities.splice(index, 1); - - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - property_indexes: propertyIndexEntities - }); - - edgeTypeStore.validateAllNewEdgeType(true); - edgeTypeStore.validateNewEdgeType( - 'propertyIndexes' - ); - - setDeletePopIndex(null); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeletePopIndex(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: closeIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeletePopIndex(index); - } - }} - childrenWrapperElement="img" - /> -
    - ) - )} - { - if ( - edgeTypeStore.newEdgeType.property_indexes.length === 0 || - edgeTypeStore.isAddNewPropertyIndexReady - ) { - edgeTypeStore.mutateNewEdgeType({ - ...edgeTypeStore.newEdgeType, - property_indexes: [ - ...edgeTypeStore.newEdgeType.property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - edgeTypeStore.validateAllNewEdgeType(true); - // set isAddNewPropertyIndexReady to false - edgeTypeStore.validateNewEdgeType( - 'propertyIndexes', - true - ); - } - }} - style={{ - cursor: 'pointer', - color: edgeTypeStore.isAddNewPropertyIndexReady - ? '#2b65ff' - : '#999', - lineHeight: '32px' - }} - > - {t('addition.edge.add-group')} - -
    -
    -
    -
    - -
    - ); -}); - -export default CreateEdge; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateProperty.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateProperty.tsx deleted file mode 100644 index b4b2d23c8..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateProperty.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; -import { Drawer, Button, Input, Select, Message } from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; - -const dataTypeOptions = [ - 'string', - 'boolean', - 'byte', - 'int', - 'long', - 'float', - 'double', - 'date', - 'uuid', - 'blob' -]; - -const cardinalityOptions = ['single', 'list', 'set']; - -const CreateProperty: React.FC = observer(() => { - const { metadataPropertyStore, graphViewStore } = useContext( - MetadataConfigsRootStore - ); - const { t } = useTranslation(); - const handleCloseDrawer = () => { - graphViewStore.setCurrentDrawer(''); - metadataPropertyStore.resetNewProperties(); - metadataPropertyStore.resetValidateNewProperty(); - }; - - return ( - { - metadataPropertyStore.validateNewProperty(); - - if (!metadataPropertyStore.isCreatedReady) { - return; - } - - await metadataPropertyStore.addMetadataProperty(); - - if ( - metadataPropertyStore.requestStatus.addMetadataProperty === - 'success' - ) { - graphViewStore.setCurrentDrawer(''); - - Message.success({ - content: t('addition.newGraphConfig.create-scuccess'), - size: 'medium', - showCloseIcon: false - }); - } - - if ( - metadataPropertyStore.requestStatus.addMetadataProperty === - 'failed' - ) { - Message.error({ - content: metadataPropertyStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - } - - metadataPropertyStore.fetchMetadataPropertyList({ fetchAll: true }); - metadataPropertyStore.resetNewProperties(); - }} - > - {t('addition.newGraphConfig.create')} - , - - ]} - > -
    -
    -
    -
    -
    - * - {t('addition.common.property-name')}: -
    - { - metadataPropertyStore.mutateNewProperty({ - ...metadataPropertyStore.newMetadataProperty, - _name: e.value - }); - - metadataPropertyStore.validateNewProperty(); - }} - originInputProps={{ - // no autofocus here, it will automatically dispatch blur action - onBlur() { - metadataPropertyStore.validateNewProperty(); - } - }} - /> -
    -
    -
    - * - {t('addition.common.data-type')}: -
    - -
    -
    -
    - * - {t('addition.common.cardinal-number')}: -
    - -
    -
    -
    -
    -
    - ); -}); - -export default CreateProperty; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateVertex.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateVertex.tsx deleted file mode 100644 index 9a40be4ee..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/CreateVertex.tsx +++ /dev/null @@ -1,1142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { cloneDeep, isUndefined } from 'lodash-es'; -import { - Drawer, - Button, - Input, - Select, - Radio, - Switch, - Checkbox, - Tooltip, - Message -} from 'hubble-ui'; - -import { Tooltip as CustomTooltip } from '../../../common'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import { - vertexRadiusMapping, - mapMetadataProperties, - formatVertexIdText -} from '../../../../stores/utils'; -import { useTranslation } from 'react-i18next'; - -import type { VertexTypeValidatePropertyIndexes } from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import HintIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import closeIcon from '../../../../assets/imgs/ic_close_16.svg'; -import { clearObserving } from 'mobx/lib/internal'; - -const CreateVertex: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const { metadataPropertyStore, vertexTypeStore, graphViewStore } = useContext( - MetadataConfigsRootStore - ); - const { t } = useTranslation(); - const [isAddNewProperty, switchIsAddNewProperty] = useState(false); - const [deletePopIndex, setDeletePopIndex] = useState(null); - const deleteWrapperRef = useRef(null); - const dropdownWrapperRef = useRef(null); - - // need useCallback to stop infinite callings of useEffect - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - // if clicked element is not on dropdown, collpase it - if ( - isAddNewProperty && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddNewProperty(false); - } - - if ( - deletePopIndex && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - setDeletePopIndex(null); - } - }, - [deletePopIndex, isAddNewProperty] - ); - - const handleCloseDrawer = () => { - graphViewStore.setCurrentDrawer(''); - vertexTypeStore.resetNewVertextType(); - vertexTypeStore.resetAddedPropertiesInSelectedVertextType(); - }; - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( - { - vertexTypeStore.validateAllNewVertexType(); - - if (!vertexTypeStore.isCreatedReady) { - return; - } - - const { - name, - properties, - primary_keys, - style, - ...rest - } = vertexTypeStore.newVertexType; - - const mappedProperties = mapMetadataProperties( - properties, - metadataPropertyStore.metadataProperties - ); - - graphViewStore.visDataSet!.nodes.add({ - ...rest, - id: name, - label: name.length <= 15 ? name : name.slice(0, 15) + '...', - vLabel: name, - properties, - value: vertexRadiusMapping[style.size], - font: { size: 16 }, - title: ` - - - ${Object.entries(mappedProperties) - .map(([key, value]) => { - const convertedValue = - value.toLowerCase() === 'text' - ? 'string' - : value.toLowerCase(); - - const primaryKeyIndex = primary_keys.findIndex( - (primaryKey) => primaryKey === key - ); - - return ``; - }) - .join('')} - `, - color: { - background: style.color, - border: style.color, - highlight: { - background: '#fb6a02', - border: '#fb6a02' - }, - hover: { background: '#ec3112', border: '#ec3112' } - }, - // reveal label when zoom to max - scaling: { - label: { - max: Infinity, - maxVisible: Infinity - } - }, - chosen: { - node( - values: any, - id: string, - selected: boolean, - hovering: boolean - ) { - if (hovering || selected) { - values.shadow = true; - values.shadowColor = 'rgba(0, 0, 0, 0.6)'; - values.shadowX = 0; - values.shadowY = 0; - values.shadowSize = 25; - } - - if (selected) { - values.size += 5; - } - } - } - }); - - await vertexTypeStore.addVertexType(); - - if (vertexTypeStore.requestStatus.addVertexType === 'success') { - vertexTypeStore.fetchVertexTypeList(); - vertexTypeStore.resetNewVertextType(); - vertexTypeStore.resetAddedPropertiesInSelectedVertextType(); - graphViewStore.setCurrentDrawer(''); - - Message.success({ - content: t('addition.newGraphConfig.create-scuccess'), - size: 'medium', - showCloseIcon: false - }); - - // if vertex is empty before, trigger re-render here to reveal - if (graphViewStore.isGraphVertexEmpty) { - graphViewStore.switchGraphDataEmpty(false); - // need to get node colors again since fetchGraphViewData() will cause re-render in - // the graph use graphNode() rather than local added node - await dataAnalyzeStore.fetchAllNodeStyle(); - graphViewStore.fetchGraphViewData( - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.vertexWritingMappings, - dataAnalyzeStore.edgeColorMappings, - dataAnalyzeStore.edgeThicknessMappings, - dataAnalyzeStore.edgeWithArrowMappings, - dataAnalyzeStore.edgeWritingMappings - ); - } - return; - } - - if (vertexTypeStore.requestStatus.addVertexType === 'failed') { - Message.error({ - content: vertexTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - } - }} - > - {t('addition.newGraphConfig.create')} - , - - ]} - > -
    -
    -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - * - {t('addition.vertex.vertex-type-name')}: -
    - { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - name: e.value - }); - }} - originInputProps={{ - onBlur() { - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType('name'); - } - }} - /> -
    -
    -
    - * - {t('addition.vertex.vertex-style')}: -
    -
    - -
    -
    - -
    -
    -
    -
    - * - {t('addition.common.id-strategy')}: -
    - ) => { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - id_strategy: e.target.value - }); - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType('primaryKeys'); - }} - > - - {t('addition.constant.primary-key-id')} - - - {t('addition.constant.automatic-generation')} - - - {t('addition.constant.custom-string')} - - - {t('addition.constant.custom-number')} - - - {t('addition.constant.custom-uuid')} - - -
    -
    -
    - {vertexTypeStore.newVertexType.id_strategy === - 'PRIMARY_KEY' && ( - * - )} - {t('addition.common.association-property')}: -
    -
    - {vertexTypeStore.newVertexType.properties.length !== 0 && ( -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.allow-null')}
    -
    - {vertexTypeStore.newVertexType.properties.map( - (property, index) => { - const currentProperties = cloneDeep( - vertexTypeStore.newVertexType.properties - ); - - return ( -
    -
    {property.name}
    -
    - { - currentProperties[ - index - ].nullable = !currentProperties[index] - .nullable; - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - properties: currentProperties - }); - }} - size="large" - /> -
    -
    - ); - } - )} -
    - )} -
    { - switchIsAddNewProperty(!isAddNewProperty); - }} - > - {t('addition.common.add-property')} - toggleAddProperty -
    -
    -
    - - {isAddNewProperty && ( -
    -
    -
    - {metadataPropertyStore.metadataProperties.map((property) => ( -
    - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesInSelectedVertextType = - vertexTypeStore.addedPropertiesInSelectedVertextType; - - addedPropertiesInSelectedVertextType.has( - property.name - ) - ? addedPropertiesInSelectedVertextType.delete( - property.name - ) - : addedPropertiesInSelectedVertextType.add( - property.name - ); - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - properties: [ - ...addedPropertiesInSelectedVertextType - ].map((propertyName) => { - const currentProperty = vertexTypeStore.newVertexType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined(currentProperty) - ? currentProperty.nullable - : true - }; - }) - }); - - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType('properties'); - }} - > - {property.name} - - -
    - ))} -
    -
    - )} - - {vertexTypeStore.newVertexType.id_strategy === 'PRIMARY_KEY' && ( -
    -
    - * - {t('addition.common.primary-key-property')}: -
    - -
    - )} - -
    -
    - * - {t('addition.vertex.vertex-display-content')}: -
    - -
    - -
    - - {t('addition.edge.index-info')} - - - hint - -
    - -
    -
    - * - {t('addition.menu.type-index')}: -
    - { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - open_label_index: !vertexTypeStore.newVertexType - .open_label_index - }); - }} - size="large" - /> -
    - -
    -
    - {t('addition.common.property-index')}: -
    -
    - {vertexTypeStore.newVertexType.property_indexes.length !== - 0 && ( -
    -
    - {t('addition.edge.index-name')} -
    -
    - {t('addition.edge.index-type')} -
    -
    {t('addition.common.property')}
    -
    - )} - {vertexTypeStore.newVertexType.property_indexes.map( - ({ name, type, fields }, index) => ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - vertexTypeStore.newVertexType.property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType( - 'propertyIndexes' - ); - } - }} - /> -
    -
    - -
    -
    - -
    - - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const propertyIndexEntities = cloneDeep( - vertexTypeStore.newVertexType - .property_indexes - ); - - propertyIndexEntities.splice(index, 1); - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - property_indexes: propertyIndexEntities - }); - - vertexTypeStore.validateAllNewVertexType( - true - ); - vertexTypeStore.validateNewVertexType( - 'propertyIndexes' - ); - - setDeletePopIndex(null); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeletePopIndex(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: closeIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeletePopIndex(index); - } - }} - childrenWrapperElement="img" - /> -
    - ) - )} - { - if ( - vertexTypeStore.newVertexType.property_indexes.length === - 0 || - vertexTypeStore.isAddNewPropertyIndexReady - ) { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - property_indexes: [ - ...vertexTypeStore.newVertexType.property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - vertexTypeStore.validateAllNewVertexType(true); - // set isAddNewPropertyIndexReady to false - vertexTypeStore.validateNewVertexType( - 'propertyIndexes', - true - ); - } - }} - style={{ - cursor: 'pointer', - color: vertexTypeStore.isAddNewPropertyIndexReady - ? '#2b65ff' - : '#999', - lineHeight: '32px' - }} - > - {t('addition.edge.add-group')} - -
    -
    -
    -
    - -
    - ); -}); - -export default CreateVertex; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/GraphView.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/GraphView.less deleted file mode 100644 index 3b01eb15e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/GraphView.less +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.metadata-graph { - &-drawer-wrapper { - .metadata-graph-drawer { - &-title { - font-family: - 'PingFangSC-Medium', - 'Microsoft YaHei Bold', - '微软雅黑', - Arial, - sans-serif; - width: 115px; - font-weight: bold; - margin-bottom: 16px; - text-align: right; - word-break: keep-all; - } - - &-manipulations, - &-options { - margin-bottom: 32px; - display: flex; - align-items: center; - - &-name { - width: 125px; - margin-right: 43.9px; - text-align: right; - color: #333; - font-size: 14px; - word-break: keep-all; - line-height: 32px; - } - - &-expands { - font-size: 14px; - color: #333; - } - } - } - } - - &-view-wrapper { - height: calc(100vh - 222px); - overflow: hidden; - } - - &-view { - margin: -1px; - } - - &-view-empty-wrapper { - height: calc(100vh - 222px); - display: flex; - justify-content: center; - align-items: center; - } - - &-view-empty { - display: flex; - flex-direction: column; - align-items: center; - font-size: 14px; - color: #333; - } - - &-loading { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100%; - } - - &-loading-bg { - width: 144px; - height: 144px; - position: relative; - margin-bottom: 10px; - } - - &-loading-back { - position: absolute; - left: 22px; - top: 20px; - } - - &-loading-front { - position: absolute; - left: 57.1px; - top: 52.1px; - animation: loading-rotate 2s linear infinite; - } - - @keyframes loading-rotate { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } - } - - &-property-manipulation { - font-size: 14px; - color: #2b65ff; - cursor: pointer; - - &:hover { - color: #527dff; - } - - &:active { - color: #184bcc; - } - } - - &-tooltips { - background: #fff; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - border-radius: 3px; - padding: 16px; - font-size: 14px; - color: #333; - - &-title { - font-size: 16px; - font-weight: 900; - margin-bottom: 16px; - } - } -} - -/* override */ - -// refer to index.less in root dir, this should be fixed in future -.metadata-graph-drawer-wrapper { - .new-fc-one-input-all-container { - position: relative; - } -} - -.metadata-graph-drawer-options-colors { - margin-left: 12px; -} - -.metadata-graph-drawer-options-color { - width: 20px; - height: 20px; -} - -.metadata-graph-drawer-options .metadata-graph-drawer-options-color { - margin-top: 6px; -} - -// refer to NewVertexType.less, color option style in dropdown -.new-fc-one-select-dropdown-menu-container .metadata-graph-drawer-options-color { - width: 20px; - height: 20px; - margin: 6px auto; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/GraphView.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/GraphView.tsx deleted file mode 100644 index bc2c17536..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/GraphView.tsx +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import { isEmpty } from 'lodash-es'; -import vis from 'vis-network'; -import 'vis-network/styles/vis-network.min.css'; -import { Button } from 'hubble-ui'; - -import CreateProperty from './CreateProperty'; -import CreateVertex from './CreateVertex'; -import CreateEdge from './CreateEdge'; -import CheckAndEditVertex from './CheckAndEditVertex'; -import CheckAndEditEdge from './CheckAndEditEdge'; -import CheckProperty from './CheckProperty'; - -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import { generateGraphModeId } from '../../../../stores/utils'; - -import AddIcon from '../../../../assets/imgs/ic_add.svg'; -import LoadingBackIcon from '../../../../assets/imgs/ic_loading_back.svg'; -import LoadingFrontIcon from '../../../../assets/imgs/ic_loading_front.svg'; -import { useTranslation } from 'react-i18next'; - -import '../../data-analyze/DataAnalyze.less'; -import './GraphView.less'; - -const styles = { - marginLeft: '12px' -}; - -const GraphView: React.FC = observer(() => { - const { - metadataPropertyStore, - vertexTypeStore, - edgeTypeStore, - graphViewStore - } = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - useEffect(() => { - metadataPropertyStore.fetchMetadataPropertyList({ - fetchAll: true - }); - - vertexTypeStore.fetchVertexTypeList({ fetchAll: true }); - edgeTypeStore.fetchEdgeTypeList({ fetchAll: true }); - - return () => { - metadataPropertyStore.dispose(); - vertexTypeStore.dispose(); - edgeTypeStore.dispose(); - }; - }, [edgeTypeStore, graphViewStore, metadataPropertyStore, vertexTypeStore]); - - return ( -
    -
    - - - - {/* .outsideClick need id to specify logic here */} - {!isEmpty(metadataPropertyStore.metadataProperties) && ( -
    - -
    - )} -
    - {/* note: components below all have graphView.currentDrawer in render - * if use && at here it will dispatch re-render in this component - * which cause all components below to re-render either - */} - - - - - - - -
    - ); -}); - -const GraphDataView: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const { - metadataPropertyStore, - vertexTypeStore, - edgeTypeStore, - graphViewStore - } = useContext(MetadataConfigsRootStore); - const [graph, setGraph] = useState(null); - const [showLoadingGraphs, switchShowLoadingGraphs] = useState(true); - const [isGraphDataLoaded, switchIsGraphLoaded] = useState(false); - const { t } = useTranslation(); - const graphWrapper = useRef(null); - const resultWrapper = useRef(null); - - const redrawGraphs = useCallback(() => { - if (graph) { - // list mode may has scrollbar - // when switch to graph mode, scrollbar hides quickly but resize event cannot dispatch - // at this scenario we have to manually calculate the width - // however when set resultWrapper style with overflow: hidden - // window.innerWidth equals to document.body.scrollWidth - // no need to write code below: - - // let width: string; - // if (window.innerWidth - document.body.scrollWidth > 0) { - // width = String(window.innerWidth - 126) + 'px'; - // } else { - // width = getComputedStyle(resultWrapper.current!).width as string; - // } - - if ( - graphViewStore.originalGraphViewData !== null && - graphViewStore.originalGraphViewData.vertices.length === 0 && - graphViewStore.originalGraphViewData.edges.length === 0 - ) { - graph.setSize('0', '0'); - graph.redraw(); - return; - } - - const width = - String( - Number( - (getComputedStyle(resultWrapper.current!).width as string).split( - 'px' - )[0] - ) + 2 - ) + 'px'; - const height = - String( - Number( - (getComputedStyle(resultWrapper.current!).height as string).split( - 'px' - )[0] - ) + 2 - ) + 'px'; - - graph.setSize(width, height); - graph.redraw(); - } - }, [graph, graphViewStore.originalGraphViewData]); - - useEffect(() => { - graphViewStore.fetchGraphViewData( - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.vertexWritingMappings, - dataAnalyzeStore.edgeColorMappings, - dataAnalyzeStore.edgeThicknessMappings, - dataAnalyzeStore.edgeWithArrowMappings, - dataAnalyzeStore.edgeWritingMappings - ); - - return () => { - graphViewStore.dispose(); - }; - }, [ - dataAnalyzeStore.colorMappings, - dataAnalyzeStore.vertexSizeMappings, - dataAnalyzeStore.vertexWritingMappings, - dataAnalyzeStore.edgeColorMappings, - dataAnalyzeStore.edgeThicknessMappings, - dataAnalyzeStore.edgeWithArrowMappings, - dataAnalyzeStore.edgeWritingMappings, - graphViewStore - ]); - - useEffect(() => { - const graphNodes = new vis.DataSet(graphViewStore.graphNodes); - const graphEdges = new vis.DataSet(graphViewStore.graphEdges); - - if (!graph) { - const data = { - nodes: graphNodes, - edges: graphEdges - }; - - const layout: vis.Options = { - nodes: { - shape: 'dot' - }, - edges: { - arrowStrikethrough: false, - color: { - color: 'rgba(92, 115, 230, 0.8)', - hover: 'rgba(92, 115, 230, 1)', - highlight: 'rgba(92, 115, 230, 1)' - }, - scaling: { - min: 1, - max: 3, - label: { - enabled: false - } - } - }, - interaction: { - hover: true - }, - physics: { - maxVelocity: 50, - solver: 'forceAtlas2Based', - forceAtlas2Based: { - avoidOverlap: 0 - }, - timestep: 0.3, - stabilization: { iterations: 150 } - } - }; - - if (graphWrapper.current !== null) { - const network = new vis.Network(graphWrapper!.current, data, layout); - - network.on('click', ({ nodes, edges }) => { - // click on node, note that edges(related) also has value - if (!isEmpty(nodes)) { - // note: cannot abstract switchClickOn...() and clearTimeout - // as common callings with node and edge, since click event - // would be dispatched even if click is not on node and edge - // dataAnalyzeStore.switchClickOnNodeOrEdge(true); - // clearTimeout(timer); - - // caution: nodeId is automatically converted to number - const nodeId = nodes[0]; - - if (graphViewStore.graphViewData !== null) { - const index = vertexTypeStore.vertexTypes.findIndex( - (vertex) => vertex.name === String(nodeId) - ); - - if (index === -1) { - return; - } - - vertexTypeStore.selectVertexType(index); - - // check also needs style infos - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - style: { - color: vertexTypeStore.selectedVertexType!.style.color, - icon: null, - size: vertexTypeStore.selectedVertexType!.style.size, - display_fields: vertexTypeStore.selectedVertexType!.style - .display_fields - } - }); - - graphViewStore.setCurrentDrawer('check-vertex'); - graphViewStore.switchNodeOrEdgeClicked(true); - - // check if vertex type being used - vertexTypeStore.checkIfUsing([ - vertexTypeStore.selectedVertexType!.name - ]); - } - - return; - } - - if (!isEmpty(edges)) { - const edgeId = edges[0]; - - if (graphViewStore.graphViewData !== null) { - const index = edgeTypeStore.edgeTypes.findIndex( - (edge) => - generateGraphModeId( - edge.name, - edge.source_label, - edge.target_label - ) === edgeId - ); - - if (index === -1) { - return; - } - - edgeTypeStore.selectEdgeType(index); - // check also needs style infos - edgeTypeStore.mutateEditedSelectedEdgeType({ - ...edgeTypeStore.editedSelectedEdgeType, - style: { - color: edgeTypeStore.selectedEdgeType!.style.color, - icon: null, - with_arrow: edgeTypeStore.selectedEdgeType!.style.with_arrow, - thickness: edgeTypeStore.selectedEdgeType!.style.thickness, - display_fields: edgeTypeStore.selectedEdgeType!.style - .display_fields - } - }); - - graphViewStore.setCurrentDrawer('check-edge'); - graphViewStore.switchNodeOrEdgeClicked(true); - } - } - }); - - network.on('dragEnd', (e) => { - if (!isEmpty(e.nodes)) { - network.unselectAll(); - } - }); - - network.once('stabilizationIterationsDone', () => { - switchShowLoadingGraphs(false); - }); - - setGraph(network); - graphViewStore.setVisNetwork(network); - } - } else { - // if graph view data arrives, init to graph - if (graphViewStore.originalGraphViewData !== null && !isGraphDataLoaded) { - // switchIsGraphLoaded(true); - - graph.setData({ - nodes: graphNodes, - edges: graphEdges - }); - - graphViewStore.setVisDataSet({ - nodes: graphNodes, - edges: graphEdges - }); - } - - redrawGraphs(); - } - }, [ - graph, - graphViewStore.graphEdges, - graphViewStore.graphNodes, - redrawGraphs, - vertexTypeStore, - edgeTypeStore, - graphViewStore, - isGraphDataLoaded - ]); - - useEffect(() => { - window.addEventListener('resize', redrawGraphs, false); - - return () => { - window.removeEventListener('resize', redrawGraphs); - }; - }, [redrawGraphs]); - - return ( - <> -
    -
    - {graphViewStore.requestStatus.fetchGraphViewData === 'pending' && ( -
    -
    - {t('addition.operate.load-background')} - {t('addition.operate.load-spinner')} -
    - {t('addition.message.data-loading')}... -
    - )} - {graphViewStore.requestStatus.fetchGraphViewData === 'success' && - showLoadingGraphs && - (!isEmpty(graphViewStore.originalGraphViewData!.vertices) || - !isEmpty(graphViewStore.originalGraphViewData!.edges)) && ( -
    -
    - {t('addition.operate.load-background')} - {t('addition.operate.load-spinner')} -
    - {t('addition.operate.rendering')}... -
    - )} - {graphViewStore.requestStatus.fetchGraphViewData === 'success' && - graphViewStore.isGraphVertexEmpty && - isEmpty(graphViewStore.originalGraphViewData!.vertices) && - isEmpty(graphViewStore.originalGraphViewData!.edges) && ( - - )} -
    - - ); -}); - -const EmptyGraphDataView: React.FC<{ hasProeprties: boolean }> = observer( - ({ hasProeprties }) => { - const { t } = useTranslation(); - return ( -
    -
    - {t('addition.message.no-metadata-notice')} -
    - {hasProeprties - ? t('addition.message.no-vertex-or-edge-notice') - : t('addition.message.no-metadata-notice')} -
    -
    -
    - ); - } -); - -export default GraphView; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/index.ts deleted file mode 100644 index 22850d2f3..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/graph-view/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import GraphView from './GraphView'; - -export { GraphView }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/index.ts deleted file mode 100644 index b242a5f99..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import MetadataConfigs from './MetadataConfigs'; - -export { MetadataConfigs }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/PropertyIndex.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/PropertyIndex.less deleted file mode 100644 index a3329baee..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/PropertyIndex.less +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.vertex-index-wrapper { - width: 100%; - background: #fff; - border-radius: 0 3px 3px 3px; - padding: 16px; -} - -.vertex-index-tab-wrapper { - display: flex; - width: 100%; - margin-top: 16px; - - & > .vertex-index-tab-index { - padding: 10px 25px; - height: 40px; - color: #333; - font-size: 14px; - text-align: center; - line-height: 20px; - cursor: pointer; - - &.active { - background-color: #fff; - color: #2b65ff; - border-radius: 3px 3px 0 0; - } - } -} - -.vertex-index-search-highlights { - background: transparent; - color: #2b65ff; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/PropertyIndex.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/PropertyIndex.tsx deleted file mode 100644 index 02502337d..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/PropertyIndex.tsx +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useState, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import Highlighter from 'react-highlight-words'; -import { motion } from 'framer-motion'; -import { Input, Table } from 'hubble-ui'; - -import { LoadingDataView } from '../../../common'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import './PropertyIndex.less'; -import i18next from '../../../../i18n'; -import { useTranslation } from 'react-i18next'; - -const variants = { - initial: { - opacity: 0 - }, - animate: { - opacity: 1, - transition: { - duration: 0.7 - } - }, - exit: { - opacity: 0, - transition: { - duration: 0.3 - } - } -}; - -const IndexTypeMappings: Record = { - SECONDARY: i18next.t('addition.menu.secondary-index'), - RANGE: i18next.t('addition.menu.range-index'), - SEARCH: i18next.t('addition.menu.full-text-index') -}; - -const PropertyIndex: React.FC = observer(() => { - const { metadataPropertyIndexStore } = useContext(MetadataConfigsRootStore); - const [preLoading, switchPreLoading] = useState(true); - const [currentTab, switchCurrentTab] = useState<'vertex' | 'edge'>('vertex'); - const { t } = useTranslation(); - const isLoading = - preLoading || - metadataPropertyIndexStore.requestStatus.fetchMetadataPropertIndexes === - 'pending'; - - const handleSearchChange = (e: React.ChangeEvent) => { - metadataPropertyIndexStore.mutateSearchWords(e.target.value); - }; - - const handleSearch = () => { - metadataPropertyIndexStore.mutatePageNumber(1); - metadataPropertyIndexStore.switchIsSearchedStatus(true); - metadataPropertyIndexStore.fetchMetadataPropertIndexes(currentTab); - }; - - const handleClearSearch = () => { - metadataPropertyIndexStore.mutateSearchWords(''); - metadataPropertyIndexStore.mutatePageNumber(1); - metadataPropertyIndexStore.switchIsSearchedStatus(false); - metadataPropertyIndexStore.fetchMetadataPropertIndexes(currentTab); - }; - - const handlePageNumberChange = (e: React.ChangeEvent) => { - metadataPropertyIndexStore.mutatePageNumber(Number(e.target.value)); - metadataPropertyIndexStore.fetchMetadataPropertIndexes(currentTab); - }; - - useEffect(() => { - setTimeout(() => { - switchPreLoading(false); - }, 500); - }, [currentTab]); - - useEffect(() => { - metadataPropertyIndexStore.fetchMetadataPropertIndexes(currentTab); - - return () => { - metadataPropertyIndexStore.dispose(); - }; - }, [currentTab, metadataPropertyIndexStore]); - - const columnConfigs = [ - { - title: - currentTab === 'vertex' - ? t('addition.vertex.vertex-type-name') - : t('addition.common.edge-type-name'), - dataIndex: 'owner', - render(text: string, records: any[], index: number) { - if (metadataPropertyIndexStore.collpaseInfo === null) { - // need highlighter here since searched result could be one row - return ( - - ); - } - - const [collpaseStartIndexes, collpaseNumbers] = - metadataPropertyIndexStore.collpaseInfo; - - const startIndex = collpaseStartIndexes.findIndex( - (indexNumber) => indexNumber === index - ); - - return startIndex !== -1 - ? { - children: ( -
    - -
    - ), - props: { - rowSpan: collpaseNumbers[startIndex] - } - } - : { - children: ( -
    - -
    - ), - props: { - rowSpan: 0 - } - }; - } - }, - { - title: t('addition.edge.index-name'), - dataIndex: 'name', - render(text: string) { - return ( -
    - -
    - ); - } - }, - { - title: t('addition.edge.index-type'), - dataIndex: 'type', - render(text: string) { - return IndexTypeMappings[text]; - } - }, - { - title: t('addition.common.property'), - dataIndex: 'fields', - render(properties: string[]) { - return ( -
    - -
    - ); - } - } - ]; - - return ( - -
    -
    { - if (currentTab !== 'vertex') { - metadataPropertyIndexStore.fetchMetadataPropertIndexes('vertex'); - } - - metadataPropertyIndexStore.mutateSearchWords(''); - metadataPropertyIndexStore.mutatePageNumber(1); - metadataPropertyIndexStore.switchIsSearchedStatus(false); - switchCurrentTab('vertex'); - switchPreLoading(true); - }} - className={ - currentTab === 'vertex' - ? 'vertex-index-tab-index active' - : 'vertex-index-tab-index' - } - > - {t('addition.vertex.vertex-index')} -
    -
    { - if (currentTab !== 'edge') { - metadataPropertyIndexStore.fetchMetadataPropertIndexes('edge'); - } - - metadataPropertyIndexStore.mutateSearchWords(''); - metadataPropertyIndexStore.mutatePageNumber(1); - metadataPropertyIndexStore.switchIsSearchedStatus(false); - switchCurrentTab('edge'); - switchPreLoading(true); - }} - className={ - currentTab === 'edge' - ? 'vertex-index-tab-index active' - : 'vertex-index-tab-index' - } - > - {t('addition.edge.edge-index')} -
    -
    -
    -
    - -
    -
    {t('addition.common.no-result')} - ) : ( - {t('addition.message.no-index-notice')} - ) - } - /> - ) - }} - dataSource={ - isLoading ? [] : metadataPropertyIndexStore.metadataPropertyIndexes - } - pagination={ - isLoading - ? null - : { - hideOnSinglePage: false, - pageNo: - metadataPropertyIndexStore.metadataPropertyIndexPageConfig - .pageNumber, - pageSize: 10, - showSizeChange: false, - showPageJumper: false, - total: - metadataPropertyIndexStore.metadataPropertyIndexPageConfig - .pageTotal, - onPageNoChange: handlePageNumberChange - } - } - /> - - - ); -}); - -export default PropertyIndex; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/index.ts deleted file mode 100644 index e851e9b3d..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property-index/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import PropertyIndex from './PropertyIndex'; - -export { PropertyIndex }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/MetadataProperties.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/MetadataProperties.less deleted file mode 100644 index cdcdbff7f..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/MetadataProperties.less +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.metadata-configs { - .metadata-properties { - &-selected-reveals { - width: 100%; - height: 40px; - padding: 0 16px; - display: flex; - background: #2b65ff; - border-radius: 3px 3px 0 0; - font-size: 14px; - align-items: center; - color: #fff; - - & > div { - margin-right: 12px; - } - - & > img { - margin-left: auto; - cursor: pointer; - } - } - - &-manipulation { - font-size: 14px; - color: #2b65ff; - cursor: pointer; - - &:hover { - color: #527dff; - } - - &:active { - color: #184bcc; - } - } - - &-search-highlights { - background: transparent; - color: #2b65ff; - cursor: pointer; - } - } -} - -.metadata-properties-modal { - &-title { - display: flex; - justify-content: space-between; - margin-bottom: 20px; - - & > img { - cursor: pointer; - } - } - - &-description { - margin-bottom: 16px; - color: #333; - } -} - -.property-status-not-used { - width: 58px; - height: 28px; - background: #f2fff4; - border: 1px solid #7ed988; - border-radius: 2px; - font-size: 14px; - color: #39bf45; - letter-spacing: 0; - line-height: 28px; - text-align: center; -} - -.property-status-is-using { - width: 58px; - height: 28px; - background: #fff2f2; - border: 1px solid #ff9499; - border-radius: 2px; - font-size: 14px; - color: #e64552; - letter-spacing: 0; - line-height: 28px; - text-align: center; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/MetadataProperties.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/MetadataProperties.tsx deleted file mode 100644 index 52537647b..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/MetadataProperties.tsx +++ /dev/null @@ -1,912 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import { intersection, size, without, values } from 'lodash-es'; -import { motion } from 'framer-motion'; -import { - Input, - Button, - Table, - Modal, - Select, - Message, - Loading -} from 'hubble-ui'; -import Highlighter from 'react-highlight-words'; - -import { Tooltip, LoadingDataView } from '../../../common'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; - -import type { MetadataProperty } from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import AddIcon from '../../../../assets/imgs/ic_add.svg'; -import CloseIcon from '../../../../assets/imgs/ic_close_16.svg'; -import WhiteCloseIcon from '../../../../assets/imgs/ic_close_white.svg'; -import './MetadataProperties.less'; -import ReuseProperties from './ReuseProperties'; -import { useTranslation } from 'react-i18next'; - -const styles = { - button: { - marginLeft: '12px', - width: 78 - }, - extraMargin: { - marginRight: 4 - }, - deleteWrapper: { - display: 'flex', - justifyContent: 'flex-end' - }, - loading: { - padding: 0, - marginRight: 4 - } -}; - -const dataTypeOptions = [ - 'string', - 'boolean', - 'byte', - 'int', - 'long', - 'float', - 'double', - 'date', - 'uuid', - 'blob' -]; - -const cardinalityOptions = ['single', 'list', 'set']; - -const variants = { - initial: { - opacity: 0 - }, - animate: { - opacity: 1, - transition: { - duration: 0.7 - } - }, - exit: { - opacity: 0, - transition: { - duration: 0.3 - } - } -}; - -const MetadataProperties: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const { t } = useTranslation(); - const metadataConfigsRootStore = useContext(MetadataConfigsRootStore); - const { metadataPropertyStore, graphViewStore } = metadataConfigsRootStore; - const [preLoading, switchPreLoading] = useState(true); - const [sortOrder, setSortOrder] = useState(''); - const [selectedRowKeys, mutateSelectedRowKeys] = useState([]); - const [isShowModal, switchShowModal] = useState(false); - - const isLoading = - preLoading || - metadataPropertyStore.requestStatus.fetchMetadataPropertyList === 'pending'; - - const currentSelectedRowKeys = intersection( - selectedRowKeys, - metadataPropertyStore.metadataProperties.map(({ name }) => name) - ); - - const printError = () => { - Message.error({ - content: metadataPropertyStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - }; - - const handleSearchChange = (e: React.ChangeEvent) => { - metadataPropertyStore.mutateSearchWords(e.target.value); - }; - - const handleSearch = async () => { - metadataPropertyStore.mutatePageNumber(1); - metadataPropertyStore.switchIsSearchedStatus(true); - await metadataPropertyStore.fetchMetadataPropertyList(); - - if ( - metadataPropertyStore.requestStatus.fetchMetadataPropertyList === 'failed' - ) { - printError(); - } - }; - - const handleClearSearch = () => { - metadataPropertyStore.mutateSearchWords(''); - metadataPropertyStore.mutatePageNumber(1); - metadataPropertyStore.switchIsSearchedStatus(false); - metadataPropertyStore.fetchMetadataPropertyList(); - }; - - const handleSelectedTableRow = ( - newSelectedRowKeys: string[], - selectedRows: MetadataProperty[] - ) => { - mutateSelectedRowKeys(newSelectedRowKeys); - // mutateSelectedRowKeys(selectedRows.map(({ name }) => name)); - }; - - const handleSortClick = () => { - switchPreLoading(true); - - if (sortOrder === 'descend') { - metadataPropertyStore.mutatePageSort('asc'); - setSortOrder('ascend'); - } else { - metadataPropertyStore.mutatePageSort('desc'); - setSortOrder('descend'); - } - - metadataPropertyStore.fetchMetadataPropertyList(); - }; - - const handlePageChange = async (e: React.ChangeEvent) => { - // mutateSelectedRowKeys([]); - metadataPropertyStore.mutatePageNumber(Number(e.target.value)); - await metadataPropertyStore.fetchMetadataPropertyList(); - - if ( - metadataPropertyStore.requestStatus.fetchMetadataPropertyList === 'failed' - ) { - printError(); - } - }; - - const batchDeleteProperties = async () => { - if ( - values(currentSelectedRowKeys).every( - (key) => metadataPropertyStore.metadataPropertyUsingStatus?.[key] - ) - ) { - Message.error({ - content: t('addition.message.no-property-can-delete'), - size: 'medium', - showCloseIcon: false - }); - - return; - } - - switchShowModal(false); - // need to set a copy in store since local row key state would be cleared - metadataPropertyStore.mutateSelectedMetadataProperyNames( - currentSelectedRowKeys - ); - // mutateSelectedRowKeys([]); - await metadataPropertyStore.deleteMetadataProperty(currentSelectedRowKeys); - // metadataPropertyStore.mutateSelectedMetadataProperyNames([]); - - if ( - metadataPropertyStore.requestStatus.deleteMetadataProperty === 'success' - ) { - Message.success({ - content: t('addition.common.del-success'), - size: 'medium', - showCloseIcon: false - }); - - mutateSelectedRowKeys( - without(selectedRowKeys, ...currentSelectedRowKeys) - ); - - await metadataPropertyStore.fetchMetadataPropertyList(); - - // fetch previous page data if it's empty - if ( - metadataPropertyStore.requestStatus.fetchMetadataPropertyList === - 'success' && - size(metadataPropertyStore.metadataProperties) === 0 && - metadataPropertyStore.metadataPropertyPageConfig.pageNumber > 1 - ) { - metadataPropertyStore.mutatePageNumber( - metadataPropertyStore.metadataPropertyPageConfig.pageNumber - 1 - ); - - metadataPropertyStore.fetchMetadataPropertyList(); - } - - return; - } - - if ( - metadataPropertyStore.requestStatus.deleteMetadataProperty === 'failed' - ) { - printError(); - } - }; - - // hack: need to call @observable at here to dispatch re-render by mobx - // since @action in onBlur() in doesn't dispatch re-render - metadataPropertyStore.validateNewPropertyErrorMessage.name.toLowerCase(); - - const columnConfigs = [ - { - title: t('addition.common.property-name'), - dataIndex: 'name', - width: '45%', - sorter: true, - sortOrder, - render(text: string, records: any, index: number) { - if (metadataPropertyStore.isCreateNewProperty === true && index === 0) { - return ( - { - metadataPropertyStore.mutateNewProperty({ - ...metadataPropertyStore.newMetadataProperty, - _name: e.value - }); - - metadataPropertyStore.validateNewProperty(); - }} - originInputProps={{ - autoFocus: 'autoFocus', - onBlur() { - metadataPropertyStore.validateNewProperty(); - } - }} - /> - ); - } - - return ( -
    - {metadataPropertyStore.isSearched.status ? ( - - ) : ( - {text} - )} -
    - ); - } - }, - { - title: t('addition.common.data-type'), - dataIndex: 'data_type', - width: '20%', - render(text: string, records: any, index: number) { - if (metadataPropertyStore.isCreateNewProperty === true && index === 0) { - return ( - - ); - } - - const realText = text === 'TEXT' ? 'string' : text.toLowerCase(); - - return ( -
    - {realText} -
    - ); - } - }, - { - title: t('addition.common.cardinal-number'), - dataIndex: 'cardinality', - width: '20%', - render(text: string, records: any, index: number) { - if (metadataPropertyStore.isCreateNewProperty === true && index === 0) { - return ( - - ); - } - - return ( -
    - {text.toLowerCase()} -
    - ); - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '15%', - align: 'right', - render(_: any, records: any, index: number) { - return ( - - ); - } - } - ]; - - useEffect(() => { - setTimeout(() => { - switchPreLoading(false); - }, 500); - }, [sortOrder]); - - useEffect(() => { - if (metadataConfigsRootStore.currentId !== null) { - metadataPropertyStore.fetchMetadataPropertyList(); - } - - return () => { - metadataPropertyStore.dispose(); - }; - }, [ - metadataPropertyStore, - metadataConfigsRootStore.currentId, - graphViewStore - ]); - - // these would be called before rendered, pre-load some data here - // useEffect(() => { - // dataAnalyzeStore.fetchAllNodeStyle(); - // dataAnalyzeStore.fetchAllEdgeStyle(); - // }, [dataAnalyzeStore]); - - if (metadataPropertyStore.currentTabStatus === 'reuse') { - return ; - } - - return ( - -
    -
    - - - -
    - {size(currentSelectedRowKeys) !== 0 && ( -
    -
    - {t('addition.message.selected')} - {size(currentSelectedRowKeys)} - {t('addition.common.term')} -
    - - {t('addition.common.close')} { - mutateSelectedRowKeys([]); - }} - /> -
    - )} -
    rowData.name} - locale={{ - emptyText: ( - {t('addition.common.no-result')} - ) : ( - - ) - } - /> - ) - }} - rowSelection={{ - selectedRowKeys, - onChange: handleSelectedTableRow - }} - onSortClick={handleSortClick} - dataSource={ - isLoading - ? [] - : metadataPropertyStore.isCreateNewProperty - ? metadataPropertyStore.reunionMetadataProperty - : metadataPropertyStore.metadataProperties - } - pagination={ - isLoading - ? null - : { - hideOnSinglePage: false, - pageNo: - metadataPropertyStore.metadataPropertyPageConfig.pageNumber, - pageSize: 10, - showSizeChange: false, - showPageJumper: false, - total: - metadataPropertyStore.metadataPropertyPageConfig.pageTotal, - onPageNoChange: handlePageChange - } - } - /> - { - switchShowModal(false); - }} - > -
    -
    - {t('addition.common.del-comfirm')} - {t('addition.common.close')} { - switchShowModal(false); - }} - /> -
    -
    - {t('addition.message.del-unused-property-notice')} -
    -
    ) { - return ( - - {text} - - ); - } - }, - { - title: t('addition.common.status'), - dataIndex: 'status', - render(isUsing: boolean) { - return ( -
    - {isUsing - ? t('addition.common.in-use') - : t('addition.common.not-used')} -
    - ); - } - } - ]} - dataSource={currentSelectedRowKeys.map((name) => { - return { - name, - status: - metadataPropertyStore.metadataPropertyUsingStatus !== - null && - // data may have some delay which leads to no matching propety value - !!metadataPropertyStore.metadataPropertyUsingStatus[name] - }; - })} - pagination={false} - /> - - - - - ); -}); - -export interface MetadataPropertiesManipulationProps { - propertyName: string; - propertyIndex: number; - // allSelectedKeys: number[]; -} - -const MetadataPropertiesManipulation: React.FC = - observer(({ propertyName, propertyIndex }) => { - const { metadataPropertyStore } = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - const [isPopDeleteModal, switchPopDeleteModal] = useState(false); - const [isDeleting, switchDeleting] = useState(false); - const deleteWrapperRef = useRef(null); - const isDeleteOrBatchDeleting = - isDeleting || - (metadataPropertyStore.requestStatus.deleteMetadataProperty === - 'pending' && - metadataPropertyStore.selectedMetadataPropertyNames.includes( - propertyName - )); - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isPopDeleteModal && - deleteWrapperRef && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchPopDeleteModal(false); - } - }, - [deleteWrapperRef, isPopDeleteModal] - ); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - if ( - metadataPropertyStore.isCreateNewProperty === true && - propertyIndex === 0 - ) { - return ( -
    - { - metadataPropertyStore.validateNewProperty(); - - if (!metadataPropertyStore.isCreatedReady) { - return; - } - - metadataPropertyStore.switchIsCreateNewProperty(false); - await metadataPropertyStore.addMetadataProperty(); - - if ( - metadataPropertyStore.requestStatus.addMetadataProperty === - 'success' - ) { - Message.success({ - content: t('addition.newGraphConfig.create-scuccess'), - size: 'medium', - showCloseIcon: false - }); - } - - if ( - metadataPropertyStore.requestStatus.addMetadataProperty === - 'failed' - ) { - Message.error({ - content: metadataPropertyStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - } - - metadataPropertyStore.fetchMetadataPropertyList(); - metadataPropertyStore.resetNewProperties(); - }} - > - {t('addition.newGraphConfig.create')} - - { - metadataPropertyStore.switchIsCreateNewProperty(false); - metadataPropertyStore.resetNewProperties(); - metadataPropertyStore.resetValidateNewProperty(); - - if (metadataPropertyStore.metadataProperties.length === 0) { - metadataPropertyStore.changeCurrentTabStatus('empty'); - } - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - {isDeleteOrBatchDeleting && ( - - )} - - {metadataPropertyStore.metadataPropertyUsingStatus && - metadataPropertyStore.metadataPropertyUsingStatus[ - propertyName - ] ? ( -

    - {t('addition.message.property-using-cannot-delete')} -

    - ) : ( - <> -

    - {t('addition.message.property-del-confirm')} -

    -

    {t('addition.message.property-del-confirm-again')}

    -

    {t('addition.message.long-time-notice')}

    -
    - - -
    - - )} -
    - } - childrenProps={{ - className: 'metadata-properties-manipulation no-line-break', - style: styles.extraMargin, - title: isDeleteOrBatchDeleting - ? t('addition.operate.del-ing') - : t('addition.common.del'), - async onClick() { - if (isDeleteOrBatchDeleting) { - return; - } - - await metadataPropertyStore.checkIfUsing([propertyName]); - - if ( - metadataPropertyStore.requestStatus.checkIfUsing === 'success' - ) { - switchPopDeleteModal(true); - } - } - }} - > - {isDeleteOrBatchDeleting - ? t('addition.operate.del-ing') - : t('addition.common.del')} - - - ); - }); - -const EmptyPropertyHints: React.FC = observer(() => { - const { metadataPropertyStore } = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - return ( -
    - Add new property -
    - {t('addition.message.property-create-desc')} -
    -
    - - -
    -
    - ); -}); - -export default MetadataProperties; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/ReuseProperties.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/ReuseProperties.less deleted file mode 100644 index 71698137e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/ReuseProperties.less +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.reuse-properties-wrapper { - width: 100%; - background: #fff; - margin-top: 16px; - padding: 16px; - - .reuse-steps { - width: 860px; - margin: 16px auto 18px; - } - - .reuse-properties { - &-row { - display: flex; - align-items: center; - margin: 18px 0 32px; - font-size: 14px; - color: #333; - - &:last-child { - margin-bottom: 24px; - } - - &-name { - width: 98px; - text-align: right; - margin-right: 13.9px; - } - } - - &-manipulations { - display: flex; - justify-content: center; - margin-bottom: 88px; - } - - &-validate { - &-duplicate, - &-exist, - &-pass { - width: 58px; - margin: 0 auto; - border-radius: 2px; - letter-spacing: 0; - line-height: 22px; - text-align: center; - } - - &-duplicate { - background: #fff2f2; - border: 1px solid #ff9499; - color: #e64552; - } - - &-exist, - &-pass { - background: #f2fff4; - border: 1px solid #7ed988; - color: #39bf45; - } - } - - &-complete-hint { - display: flex; - flex-direction: column; - justify-content: center; - margin: 88px auto 327px; - - &-description { - display: flex; - justify-content: center; - - & > div { - margin-left: 20px; - } - } - - &-manipulations { - margin: 42px auto 0; - } - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/ReuseProperties.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/ReuseProperties.tsx deleted file mode 100644 index 032257db3..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/ReuseProperties.tsx +++ /dev/null @@ -1,542 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useState, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { - Select, - Steps, - Transfer, - Button, - Table, - Input, - Message -} from 'hubble-ui'; -import { cloneDeep } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; - -import { GraphManagementStoreContext } from '../../../../stores'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; - -import PassIcon from '../../../../assets/imgs/ic_pass.svg'; - -import './ReuseProperties.less'; - -const ReuseProperties: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const metadataConfigsRootStore = useContext(MetadataConfigsRootStore); - const { metadataPropertyStore } = metadataConfigsRootStore; - const { t } = useTranslation(); - const [currentStatus, setCurrentStatus] = useState(1); - // acutally the name, not id in database - const [selectedId, mutateSelectedId] = useState<[] | string>([]); - const [selectedList, mutateSelectedList] = useState([]); - - // step 2 - const [editIndex, setEditIndex] = useState(null); - - // hack: need to call @observable at here to dispatch re-render by mobx - // since @action in onBlur() in doesn't dispatch re-render - metadataPropertyStore.validateRenameReusePropertyErrorMessage.name.toUpperCase(); - - const columnConfigs = [ - { - title: t('addition.common.property-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (editIndex !== index) { - return ( -
    - {text} -
    - ); - } - - return ( - { - const editedCheckedReusableProperties = cloneDeep( - metadataPropertyStore.editedCheckedReusableProperties! - ); - - editedCheckedReusableProperties.propertykey_conflicts[ - index - ].entity.name = e.value; - - metadataPropertyStore.mutateEditedReusableProperties( - editedCheckedReusableProperties - ); - }} - originInputProps={{ - onBlur() { - metadataPropertyStore.validateRenameReuseProperty(index); - } - }} - /> - ); - } - }, - { - title: t('addition.common.data-type'), - dataIndex: 'data_type', - width: '15%', - render(text: string) { - if (text === 'TEXT') { - return ( -
    - string -
    - ); - } - - return ( -
    - {text.toLowerCase()} -
    - ); - } - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - align: 'center', - width: '15%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if ( - metadataPropertyStore.reusablePropertyNameChangeIndexes.has(index) - ) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return
    {text}
    ; - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - if (index === editIndex) { - return ( -
    - { - const isReady = metadataPropertyStore.validateRenameReuseProperty( - index - ); - - if (isReady) { - setEditIndex(null); - - metadataPropertyStore.mutateReusablePropertyNameChangeIndexes( - index - ); - } - }} - > - {t('addition.common.save')} - - { - setEditIndex(null); - metadataPropertyStore.resetValidateRenameReuseProperty(); - metadataPropertyStore.resetEditedReusablePropertyName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (editIndex !== null) { - return; - } - - setEditIndex(index); - }} - > - {t('addition.operate.rename')} - - { - if (editIndex !== null) { - return; - } - - setEditIndex(null); - - const editedCheckedReusableProperties = cloneDeep( - metadataPropertyStore.editedCheckedReusableProperties! - ); - - editedCheckedReusableProperties.propertykey_conflicts.splice( - index, - 1 - ); - - // remove selected status of the property in - mutateSelectedList( - [...selectedList].filter( - (property) => - property !== - metadataPropertyStore.editedCheckedReusableProperties! - .propertykey_conflicts[index].entity.name - ) - ); - - // remove property in Table - metadataPropertyStore.mutateEditedReusableProperties( - editedCheckedReusableProperties - ); - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - useEffect(() => { - return () => { - // reset validate error message before unmount to avoid conflict with other component validation - metadataPropertyStore.resetValidateRenameReuseProperty(); - }; - }, [metadataPropertyStore]); - - return ( -
    -
    - {t('addition.operate.reuse-property')} -
    -
    - - {[ - t('addition.menu.select-reuse-item'), - t('addition.menu.confirm-reuse-item'), - t('addition.menu.complete-reuse') - ].map((title: string, index: number) => ( - index + 1 - ? 'finish' - : 'wait' - } - key={title} - /> - ))} - - - {currentStatus === 1 && ( - <> -
    -
    - * - {t('addition.newGraphConfig.id')}: -
    - -
    -
    -
    - * - {t('addition.operate.reuse-property')}: -
    - name - )} - selectedList={selectedList} - showSearchBox={false} - candidateTreeStyle={{ - width: 359, - fontSize: 14 - }} - selectedTreeStyle={{ - width: 359, - fontSize: 14 - }} - handleSelect={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleSelectAll={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleDelete={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleDeleteAll={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - /> -
    -
    - - -
    - - )} - - {currentStatus === 2 && ( - <> -
    - {t('addition.common.selected-property')} -
    -
    ({ - name: entity.name, - data_type: entity.data_type, - status - }) - ) - : [] - } - pagination={false} - /> -
    - - -
    - - )} - - {currentStatus === 3 && ( -
    -
    - {t('addition.message.reuse-complete')} -
    -
    {t('addition.message.reuse-complete')}
    -
    {t('addition.message.reuse-property-success')}
    -
    -
    -
    - - -
    -
    - )} - - - ); -}); - -export default ReuseProperties; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/index.ts deleted file mode 100644 index f83cd5d41..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/property/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import MetadataProperties from './MetadataProperties'; -import ReuseProperties from './ReuseProperties'; - -export { MetadataProperties, ReuseProperties }; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/NewVertexType.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/NewVertexType.less deleted file mode 100644 index 362db1ec2..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/NewVertexType.less +++ /dev/null @@ -1,1637 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.new-vertex-type-wrapper { - width: 100%; - background: #fff; - margin-top: 16px; - padding: 30px 0; - display: flex; - flex-direction: column; - align-items: center; - - .new-vertex-type { - &-title { - // prettier-ignore - font-family: - 'PingFangSC-Medium', - 'Microsoft YaHei Bold', - '微软雅黑', - Arial, - sans-serif; - width: 115px; - font-weight: bold; - margin-bottom: 16px; - text-align: right; - word-break: keep-all; - } - - &-manipulations, - &-options { - margin-bottom: 32px; - display: flex; - align-items: center; - - &-name { - width: 125px; - margin-right: 43.9px; - text-align: right; - color: #333; - font-size: 14px; - word-break: keep-all; - line-height: 32px; - } - - &-expands { - font-size: 14px; - color: #333; - } - } - } -} - -.metadata-drawer-options-name-edit { - line-height: 34px; -} - -// retrive from parent since drawer doesn't hangs on body element -.new-vertex-type-options-colors, -.new-vertex-type-options-sizes, -.new-vertex-type-options-arrows { - margin-right: 12px; -} - -.new-vertex-type-options-color { - width: 23px; - height: 23px; - line-height: 23px; -} - -.new-vertex-type-select { - width: 20px; - height: 20px; - line-height: 20px; -} - -.new-vertex-type-options-border { - border: #dae7fb solid 2px; -} - -.new-vertex-type-options-no-border { - border: white solid 2px; -} - -.new-vertex-type-options-no-border:hover { - border: #f5f5f5 solid 2px; -} - -.new-vertex-type-options { - margin-top: 5px; -} - -.new-fc-one-select-dropdown-menu-container .new-vertex-type-options-color { - width: 14px; - height: 14px; - margin: auto; -} - -/* override */ -.new-vertex-type-options-sizes .new-fc-one-select-dropdown-menu-container { - padding: 4px 0; -} - -/* stylelint-disable */ - -/** - * Colors - */ - -/* Brand colors */ - -/* Contextual colors */ - -/* Gray scale colors */ - -/** - * Typography - */ - -/** - * Spacing - */ - -/** - * Background colors - */ - -/** - * Borders - */ - -/* Border colors */ - -/* Separator colors */ - -/* Border radii */ - -/* Shadows */ - -/* Icons */ - -/* Font sizes */ - -/* Metrics */ - -/* Stylistic variants */ - -/* Focus rings */ - -/* Button group separator */ - -/* Font sizes */ - -/* Text decorations */ - -/* Stylistic variants */ - -/* Typography */ - -/* Metrics */ - -/* Colors */ - -/* Focus ring */ - -/* Checkbox group */ - -/* Typography */ - -/* Metrics */ - -/* Colors */ - -/* Focus ring */ - -/* Strong variant */ - -/* Checkbox group */ - -/* Metrics */ - -/* Colors */ - -/* Typography */ - -/* Font sizes */ - -/* Metrics */ - -/* Colors & states */ - -/* Focus rings */ - -/* Typography */ - -/* Metrics */ - -/* Character count */ - -/* Metrics */ - -/* Font sizes */ - -/* Metrics */ - -/* States */ - -/* Option group title */ - -/* Option dropdown */ - -/* Widths */ - -/* Metrics */ - -/* Colors */ - -/* Metrics */ - -/* Colors */ - -/* Typography */ - -/* Markers */ - -/* Spacing */ - -/* Statuses */ - -/* Metrics */ - -/* Colors */ - -/* Typography */ - -/* Metrics */ - -/* Colors */ - -/* Metrics */ - -/* Colors & States */ - -/* Metrics */ - -/* Headers */ - -/* Colors */ - -/* Metrics */ - -/* Typography */ - -/* Container */ - -/* Metrics */ - -/* Colors */ - -/* Indicators */ - -/* Stylistic variants */ - -/* Metrics */ - -/* Colors */ - -/* Metrics */ - -/* Colors */ - -/* Metrics */ - -/* Stylistic variants */ - -/* Metrics */ - -/* Colors */ - -/* Metrics */ - -/* Colors */ - -/* Pages */ - -/* Indicators */ - -/* Controls */ - -/* Pages */ -button::-moz-focus-inner, -input[type='reset']::-moz-focus-inner, -input[type='button']::-moz-focus-inner, -input[type='submit']::-moz-focus-inner, -input[type='file'] > input[type='button']::-moz-focus-inner { - border: none; -} - -@keyframes loadingCircle { - 0% { - transform-origin: 50% 50%; - transform: rotate(0deg); - } - - 100% { - transform-origin: 50% 50%; - transform: rotate(360deg); - } -} - -@font-face { - font-family: 'new-fc-one-icon'; - src: url('data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBk0AAAC8AAAAYGNtYXDSvtNiAAABHAAAAGxnYXNwAAAAEAAAAYgAAAAIZ2x5ZgPgaIMAAAGQAAAuDGhlYWQWOP+LAAAvnAAAADZoaGVhCAsEPgAAL9QAAAAkaG10eMUSBrgAAC/4AAAA0GxvY2EkaxgIAAAwyAAAAGptYXhwAD8BCQAAMTQAAAAgbmFtZV3n5/sAADFUAAABenBvc3QAAwAAAAAy0AAAACAAAwPxAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpMgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAUAAAABAAEAADAAAAAQAg6SHpI+kr6TL//f//AAAAAAAg6QDpI+kl6S3//f//AAH/4xcEFwMXAhcBAAMAAQAAAAAAAAAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAACAEv/wAPLA6sAGwApAAABERQGIyImNREBBiYnJjY3ATYyFwEeAQcOAScBJSImNTQ2MyEyFhUUBiMCNRkREhn+pw4jCwwCDgGgDCAMAaANAgsMIw3+pv5LEhkZEgMVEhkZEgK4/TMSGRkSAs3+0wwCDg0jDAFqCwv+lgwjDQ4CDAEtnRkSEhkZEhIZAAMAAAA9BAADqwBEAHkAngAANxYUBwYiJyYnLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBgcGIicmNDc2Nz4BNzY1NCcuAScmIyIHDgEHBhUUFx4BFxYXNx4BBw4BJy4BNTQ3PgE3NjMyFx4BFxYVFAYHBiYnJjY3PgE1NCcuAScmIyIHDgEHBhUUFhc3FgYHBiYnLgE1NDYzMhYVFAYHDgEnLgE3PgE1NCYjIgYVFBYX0wwMDSMNJBscJwoKKCiLXl1qal1eiygoCgonHRwlDCQMDAwfFxghCAkiIXROTlhYTk50ISIJCCAXGB58DQENDCMNMjcbGl0+PkdHPj5dGhs4NA0jDAwCDCcqFBRGLi81NS8uRhQUKSZyCwIODSQLFRZkR0dkGRYMJA0MAgwMDDIjIzILC3wNIw0MDCQqKlwyMTRqXl2LKCkpKItdXmo0MjJdKiokDA0MJAweIyNNKiorWU1OdCIhISJ0Tk1ZKykqTSMiHn0MIw0NAQwwfkZGPT5bGxoaG1s+PUZHgC8MAQ0NJAwjXzU0Li5EFBQUFEQuLjQ0XiRxDSMMCwIOFzwgSWZmSSI+GA0BDAwjDQwhESY0NCYRHg0AAAMAKwFzA9UCFQALABgAJAAAASImNTQ2MzIWFRQGISImNTQ2MzIWFRQGIyEiJjU0NjMyFhUUBgOEITAwISIvL/5aIi8vIiIvLyL+fCIvLyIhMDABczAhIi8vIiEwMCEiLy8iITAwISIvLyIhMAAAAAADAAAAQAQAA0AADgAVACUAABMRFBYzITI2NREBBiInASUhARYyNwElITIWFREUBiMhIiY1ETQ2VRkSAwASGf6oJF4k/qgDFP0uAU4MHgwBTv0XAwA1S0s1/QA1S0sCsv4OEhkZEgHy/tgfHwEoOf7fCgoBIVVLNf4ANUtLNQIANUsAAAADAAD/wAQAA8AAIAA8AFkAAAE3NjIXFhQPARcWFAcGIi8BBwYiJyY0PwEnJjQ3NjIfAQMiJy4BJyY1NDc+ATc2MzIXHgEXFhUUBw4BBwYnMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWMwILrA0jDQwMra0MDA0jDaytDCQMDQ2srA0NDCQMrQtqXV6LKCgoKIteXWpqXV6LKCgoKIteXWpYTk50ISIiIXROTlhYTk50ISIiIXROTlgB8qwNDQwkDK2sDSMNDAytrQwMDSMNrK0MJAwNDaz9zigoi15dampdXosoKCgoi15dampdXosoKFUiIXROTlhYTk50ISIiIXROTlhYTk50ISIABAAA/8AEAAPAABsAOABHAFYAAAUiJy4BJyY1NDc+ATc2MzIXHgEXFhUUBw4BBwYnMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWMwM0NjMyFhURFAYjIiY1ERE0NjMyFh0BFAYjIiY9AQIAal1eiygoKCiLXl1qal1eiygoKCiLXl1qWE5OdCEiIiF0Tk5YWE5OdCEiIiF0Tk5YKxkSEhkZEhIZGRISGRkSEhlAKCiLXl1qal1eiygoKCiLXl1qal1eiygoVSIhdE5OWFhOTnQhIiIhdE5OWFhOTnQhIgKrEhkZEv6rEhkZEgFV/isRGRkRFhEZGREWAAAABQAr/8AD1QPAABIAGQAuAD0ASwAAASEiBhURFBYzITI2NREjIiY9AQUnFRQWOwEBITIWFwUeARURFAYjISImNRE0NjMTIiY1NDYzITIWFRQGIyETIiY1NDY7ATIWFRQGIwJV/lYSGRkSAqoSGcQrPAEAqgoHmf1WAdwIDgYBJAcHSzX9VjVLSzViERkZEQH7EhkZEv4FAxIZGRLjERkZEQNrGRL9ABIZGRICFTwrr8CTggcKARUFBf0GEQn9pzVLSzUDADVL/Y4ZEhIZGRISGQEAGRISGRkSEhkAAAIAQP/AA8ADwAApADgAADcVMzIWFRQGKwEiJjU0NjMRNDYzMhYVITIWHwEzMhYVERQGKwEiJi8BITUhMhYfATMRIyImLwEhEcAVEhkZEmoSGRkSGRESGQG+BQoEPZ0jMjIjpwUKBD3+TAG+BQoEPZ2nBQoEPf5MzbgZERIZGRIRGQOAEhkZEgICHjIk/eMjMgICHlYCAx4CHQIDHv3jAAADAAAAAAP+A2sAIwA6AEIAAAEzMhYVFAYHAw4BIyEiJjU8ATcuATURNDY7ATIWHwEhMhYdASM1NCYjISImLwEuASsBIgYVETc+ATMhAR4BMyETIQMDlSkbJQEBegYiFv0HGyUBBgZLNbcbOhFAATg1S1UZEv60CREGTAYYCLcSGSkFIxYChP0oBQwHArNv/StlAlUlGwQIBP4rFRslGwMFAg0bDgJrNUsbFElLNR4eERkIB1gGCxkR/pGoFhv+CAQEAav+XQAAAAIAAAAABAADawAYAC4AABMRFBYzITI2NRE0JiMhIiYvAS4BIyEiBhUlITIWFREUBiMhIiY1ETQ2MyEyFh8BVRkSAwASGRkS/rQJEQZMBhgJ/t8SGQHzATg1S0s1/QA1S0s1ASEbOhJAAuv9lRIZGRIB8hIZCAdYBgsZEQhMNf4ONUtLNQJrNUsbFEkAAgAO/+sD8gNwAB0APgAAJTI2NRE0NjsBCQEzMhYVERQWOwE1NDY7ATIWHQEzExEUBisBIiY9ASMVFAYrASImNREjIiY3ATYyFwEWBisBAwASGRkREv6Z/pkSERkZEmsyI4AjMmuASzWVEhmAGRKVNUtVHhUWAbkYQhgBuRYVHlVAGRIBVRIZAUb+uhkS/qsSGasjMjIjqwFV/tY1SxkR1tYRGUs1ASo3FAGQFhb+cBQ3AAAABAAA/8AEAAPAABsAOABHAFYAAAEiBw4BBwYVFBceARcWMzI3PgE3NjU0Jy4BJyYHMhceARcWFRQHDgEHBiMiJy4BJyY1NDc+ATc2MwMUFjMyNjURNCYjIgYVEREUFjMyNj0BNCYjIgYdAQIAal1eiygoKCiLXl1qal1eiygoKCiLXl1qWE5OdCEiIiF0Tk5YWE5OdCEiIiF0Tk5YKxkSEhkZEhIZGRISGRkSEhkDwCgoi15dampdXosoKCgoi15dampdXosoKFUiIXROTlhYTk50ISIiIXROTlhYTk50ISL9VRIZGRIBVRIZGRL+qwHVERkZERYRGRkRFgAAAQHVAZUEAAPAABoAAAEUBiMiJjU0Jy4BJyYjIiY1NDYzMhceARcWFQQAGRIRGSIhdE5OWBIZGRJqXV6LKCgBwBIZGRJYTk50ISIZERIZKCiLXl1qAAAAAAEAKwGVA9UB6wAOAAATIiY1NDYzITIWFRQGIyFVERkZEQNWERkZEfyqAZUZEhIZGRISGQABABX/1QPrA6sAIAAAARE0NjMyFhURITIWFRQGIyERFAYjIiY1ESEiJjU0NjMhAdUZEhIZAZUSGRkS/msZEhIZ/msSGRkSAZUB6wGVEhkZEv5rGRISGf5rEhkZEgGVGRISGQAABAAA/8AEAAPAABsAOABcAGsAAAUiJy4BJyY1NDc+ATc2MzIXHgEXFhUUBw4BBwYnMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWMxMVFAYjIiY9ATQ2MzI2NTQmIyIGFRQGIyImNTQ2MzIWFRQGBwc0NjMyFh0BFAYjIiY9AQIAal1eiygoKCiLXl1qal1eiygoKCiLXl1qWE5OdCEiIiF0Tk5YWE5OdCEiIiF0Tk5YKxkSEhkZEiw/PywsPxkREhlxT09xVUBWGRISGRkSEhlAKCiLXl1qal1eiygoKCiLXl1qal1eiygoVSIhdE5OWFhOTnQhIiIhdE5OWFhOTnQhIgF6VhIZGRJ8ERk8Kio8PCoSGRkSTm1tTkNlDs8SGRkSFRIZGRIVAAAAAgAA/8wD9APAACsASAAAJRcWFAcGIi8BBgcOAQcGIyInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBgcFMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWMwNQpAwMDSMNox8kI04qKixjWFeDJSYmJYNXWGNjWFeDJSYIBx0VFRr+kFJISGsfHx8fa0hIUlJISGsfHx8fa0hIUqyjDSMNDAykGhUVHQcIJiWDV1hjY1hXgyUmJiWDV1hjLCoqTiMkH1cfH2tISFJSSEhrHx8fH2tISFJSSEhrHx8AAAMAAP/ABAADwAAbADgASwAABSInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBicyNz4BNzY1NCcuAScmIyIHDgEHBhUUFx4BFxYzAwE2MhcWFAcBBiIvASY0NzYyFwIAal1eiygoKCiLXl1qal1eiygoKCiLXl1qWE5OdCEiIiF0Tk5YWE5OdCEiIiF0Tk5YVQE3DCQMDQ3+qw0jDaoNDQwkDEAoKIteXWpqXV6LKCgoKIteXWpqXV6LKChVIiF0Tk5YWE5OdCEiIiF0Tk5YWE5OdCEiAT0BNwwMDSMN/qsMDKsMJAwNDQACAAv/1QP1A6sANgBtAAA3FRQGIyImNRE0NjM6ARc2MjMyFhUUFx4BFxYzMjc+ATc2Nz4BFx4BBwYHDgEHBiMiJy4BJyYnATU0NjMyFhURFAYjKgEnBiIjIiY1NCcuAScmIyIHDgEHBgcOAScuATc2Nz4BNzYzMhceARcWF2AZEhEZGRECAgIBAwESGSAfbkpKVDw5OGAmJxgHIRARDAceLi51RERJQj09aywsIQNAGRIRGRkRAgICAQMBEhkgH25KSlQ8OThgJicYByEQEQwHHi4udURESUI9PWssLCG8ZxEZGREBaxIZAQEZElRKSm4fIBERPywsNhANBwghEEE2NUwVFREQPSorNAIIZxEZGRH+lRIZAQEZElRKSm4fIBERPywsNhANBwghEEE2NUwVFREQPSorNAAIAAD/wAQAA8AAGwA3ADsAQABOAFwAagB5AAAFMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWNyInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBgEhESETESERIRczMjY1NCYrASIGFRQWFzMyNjU0JisBIgYVFBYXMzI2NTQmKwEiBhUUFhczMjY1NCYrASIGFRQWMwIAal1eiygoKCiLXl1qal1eiygoKCiLXl1qY1dYgiUmJiWCWFdjY1dYgiUmJiWCWFf+1wGM/nQhAUr+tkLGBwoKB8YHCgoHxgcKCgfGBwoKB8YHCgoHxgcKCgd3BwkJB3cHCgoHQCgoi15dampdXosoKCgoi15dampdXosoKCEmJYJYV2NjV1iCJSYmJYJYV2NjV1iCJSYC1/4QAc/+UgGubgkHBwoKBwcJVwoHBwkJBwcKVgoHBgoKBgcKVgkHBwoKBwcJAAAIAAD/wAQAA8AAGwA3AEsAWgBpAHgAlACwAAAlMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWNyInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBgMuAQcOAR8BFjI/ATYmJyYGDwEnBzMyNjU0JisBIgYVFBYzFTMyNjU0JisBIgYVFBYzNxUUFjMyNj0BNCYjIgYVEzI3PgE3NjU0Jy4BJyYjIgcOAQcGFRQXHgEXFjciJy4BJyY1NDc+ATc2MzIXHgEXFhUUBw4BBwYCADMtLkMTFBQTQy4tMzMtLkMTFBQTQy4tMywoJzoREREROicoLCwoJzoREREROicodAQOBQUBBVQFDgVUBQEFBQ4ESEgXvgYKCga+BgoKBr4GCgoGvgYKCgZOCgcHCgoHBwoRal1eiygoKCiLXl1qal1eiygoKCiLXl1qY1dYgiUmJiWCWFdjY1dYgiUmJiWCWFfIFBNDLi0zMy0uQxMUFBNDLi0zMy0uQxMUIREROicoLCwoJzoREREROicoLCwoJzoREQFrBQEFBQ0FXwUFXwUNBQUBBVFRegkHBwoKBwcJVQoHBwkJBwcKZb0HCgoHvQcKCgf91igoi15dampdXosoKCgoi15dampdXosoKCEmJYJYV2NjV1iCJSYmJYJYV2NjV1iCJSYAAAAHAAD/wAQAA8AAJgAqAC4AMgBKAGYAggAAJSImNRE0NjMhMhYdARQGIyImPQE0JiMhIgYVERQWOwEyFhUUBisBEzMVIxUzFSM1MxUjFyImLwEmNDc2Mh8BNz4BFx4BDwEOASsBAzI3PgE3NjU0Jy4BJyYjIgcOAQcGFRQXHgEXFjciJy4BJyY1NDc+ATc2MzIXHgEXFhUUBw4BBwYBRxMbGxMBMRMbCQYGCQkH/s8GCQkGTAcJCQdML9jYPj6amsgDBQNcBAQFDAVPjwQNBQUCBJkCBgQBPmpdXosoKCgoi15dampdXosoKCgoi15damNXWIIlJiYlglhXY2NXWIIlJiYlglhX2RsTAXITGxsTfQcJCQd9BgkJBv6OBgkJBwYJAXIfmh57H/YCA1wEDQUEBFDJBQIEBAwF2AIE/uYoKIteXWpqXV6LKCgoKIteXWpqXV6LKCghJiWCWFdjY1dYgiUmJiWCWFdjY1dYgiUmAAcAAP/ABAADwAAbADcAbgB4AIoAmgCrAAAFMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWNyInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBhMhIgYVFBY7ARUUFjsBBwYWFxQyMxY2PwE+ATUzFBYfAR4BNz4BJzA0IyczMjY9ATMyNjU0JiMDFAYjISImPQEhBSMiJj0BNDY7ATIWHQEUBiMxMyMiJj0BNDY7ATIWHQEUBisBIiY9ATQ2OwEyFh0BFAYjAgBqXV6LKCgoKIteXWpqXV6LKCgoKIteXWpjV1iCJSYmJYJYV2NjV1iCJSYmJYJYV2z+WAYICAYOEAx6IwMCBQEBBQwEJwEBHQEBJwQMBgQEAgEjegwQDwYICAYrCAb+yQYIAVP++gYEBgYEBgUGBgW8BgQHBwQGBAcHYgYEBwcEBgUGBgVAKCiLXl1qal1eiygoKCiLXl1qal1eiygoISYlglhXY2NXWIIlJiYlglhXY2NXWIIlJgKlCQYFCP8MEEEECgMBAwMFSAEDAgIDAUgFAwMCCQUCQRAM/wgGBgj+8wYICAbx+gUDVQQEBARVAwUGBKMEBQUEowQGBQO+AwUFA74DBQAFAAD/wAQAA8AAGwA3AFwAgQC8AAAFMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWNyInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBgEVFAYjIiY9ATQ2MzIWFRQXHgEXFjMyNjc+ARceAQcOASMiJiclNTQ2MzIWHQEUBiMiJjU0Jy4BJyYjIgYHDgEnLgE3PgEzMhYXByMiJjU0NjsBJyY0NzYWHwE3PgEXHgEPATMyFhUUBisBFTMyFhUUBisBFRQGIyImPQEjIiY1NDY7ATUCAGpdXosoKCgoi15dampdXosoKCgoi15damNXWIIlJiYlglhXY2NXWIIlJiYlglhX/sYJBwcKCgcHCREROicoLEBqGgMNBgYFAx56SkVyIAGuCQcHCgoHBwkRETonKCxAahoDDQYGBQMeekpFciDfUAcKCgc7PQQFBQ4FSUoEDgUFAQU9PAcJCQdQUAcJCQdQCgcHCVAHCgoHUEAoKIteXWpqXV6LKCgoKIteXWpqXV6LKCghJiWCWFdjY1dYgiUmJiWCWFdjY1dYgiUmAXQ6BwoKB7YGCgoGLScnOhERRToGBQMDDQZCUEQ49zoHCgoHtQcKCgcsJyc6ERFFOgYFAwMNBkJQRDhyCgYHCkQFDgQFAQVSUgUBBQQOBUQKBwYKNAoGBwpaBgoKBloKBwYKNAAFAAD/wAQAA8AAGwBLAIsAyAEGAAABIgcOAQcGFRQXHgEXFjMyNz4BNzY1NCcuAScmEw4BBw4BIyImJy4BJy4BJy4BNTQ2Nz4BNz4BNz4BMzIWFx4BFx4BFx4BFRQGBw4BAQczLgEnLgE1MxQGBzMuATczHgEXNQ4BKwE1NDY3PgE1LgEnHgEdASM1NDY3PgE1LgEnHgEdASMGJicVPgE7AQE1PgE3PgEzNQ4BJyMiJicuASMVMz4BOwEOAQcVIwYmJxU+ATM+ATsBFRYGJx4BFz4BNzUzMhYXNQ4BKwElPgE3BjY/ATMyFhc1DgEnIz4BNz4BNycuAScOAQcjBiYnIiY1FTM+ATsBDgEHHgEXMz4BNxUUBgczLgE3NQIAal1eiygoKCiLXl1qal1eiygoKCiLXl3SH0coKVgtLVgpKEcfHzARERISEREwHx9HKClYLS1YKShHHx8wERESEhERMP4gBUMBAwECAZEDA0MEAwJbGykPCykfWwMCAgEDIyADA5EEBAECBCMfAwJDHS0REi0cQwEaGCQMCx4VESkZtAsZDwkOBAgTJBKWEicVYB4pDAYPCAcbFGADJyoKDQQzNAJNGCkQCCkgTf75AwsICxAMH/waKxAQLR7bBAwIAgQCJgYPCAMNC3sYKhECAQUSKBdgL2Q2DRYKAR8tDwMCQwQDAQPAKCiLXl1qal1eiygoKCiLXl1qal1eiygo/MQfMBEREhIRETAfH0coKVgtLVgpKEcfHzARERISEREwHx9HKClYLS1YKShHAec1AQcFCxQJFRsFDBoPAQMBNQIDDQsTCAMEAQEDAQUYExAQCREGAgICAQQDCxgODQECBDUCA/7SEg8UBQYFOAQCAQECAQE1AwIKFQklAgMENgIBAQJOEQsHDyISBhYPewMDNgMCWwQMCA8VDygCAzYEAwEIDgUBBAMLAgYCCRwTAQEDAQI2AwI/WxwLFgwYLRamHCcLByUf3AAAAAAKAAD/wAQAA8AAFwAlADwASgBeAG0AfACKAKYAwgAAJTI3PgE3NjU0Ji8BIwcOARUUFx4BFxYzAzMeARUUBiMiJjU0Njc/ATYWDwEOASsBIiYvASY2HwE3NjIfAQczNwcGIi8BBwYiLwEXFyYiBwYUHwEWMj8BNiYnJiIPAScHMzI2NTQmKwEiBhUUFjMVMzI2NTQmKwEiBhUUFjM3FRQWMzI2PQE0JiMiBhMyNz4BNzY1NCcuAScmIyIHDgEHBhUUFx4BFxY3IicuAScmNTQ3PgE3NjMyFx4BFxYVFAcOAQcGAgA1Li1CExNVVASVBFRWExNCLS41QYFMS3leXnlLS3xECw4FMgIIBJUFCAIxBQ4KRSwDBwMsc4AcJgIGAysrAgYDJhwTBQ0FBgQ6BQ8FOQUBBQUOBC4tHZUGCgoGlQcKCgeVBgoKBpUHCgoHOgkHBwoKBwcJCGpdXosoKCgoi15dampdXosoKCgoi15damNXWIIlJiYlglhXY2NXWIIlJiYlglhXuA4ONCUkLTFyQQMCNnI9LSQlNA4OAYw7ZCdKW1tKMmQwbxUDEQpjBAUFBGMKEQMVFAEBFE44CwECExMCAQs4aQYEBQ0FQgYGQgUNBQQGMzNlCQcHCgoHBwlTCgcGCgoGBwpbhAcKCgeEBwkJ/hooKIteXWpqXV6LKCgoKIteXWpqXV6LKCghJiWCWFdjY1dYgiUmJiWCWFdjY1dYgiUmAAAFAAD/wAQAA8AAGwA3AFIAfQCbAAAFMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWNyInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBhMHBiInJjQ/ASMiJjU0NjsBMhYdARQGIyImNScyFhUUBiMiBhUUFjMyNjU0NjMyFhUUBw4BBwYjIicuAScmNTQ3PgE3NjMHNhYXFAYHDgEVFBYzMjY3PgEXHgEHDgEjIiY1NDYCAGpdXosoKCgoi15dampdXosoKCgoi15damNXWIIlJiYlglhXY2NXWIIlJiYlglhXPowFDwUFBYo+CAoKCGsHCwsHCAqhBwsLB0ppaUpKaQoIBwsRETonKCwsKCc6ERERETonKCwNBwwBCQcbJCoeGykDAQwHBwoBBT0pLD81QCgoi15dampdXosoKCgoi15dampdXosoKCEmJYJYV2NjV1iCJSYmJYJYV2NjV1iCJSYCaIwFBQUPBYsLBwcLCwdsBwsLB5ALBwgKaUpKaWlKBwsLBywoJzoREREROicoLCwoJzoREWwBCQcICwEEKBseKiQbBwkBAQsIKDY/LSg9AAAABQAA/8AEAAPAABwAPwBKAFkAaAAAARUUBiMiJj0BIRUUBiMiJj0BIyIGHQEhNTQmKwE1MzIWFREUBiMhIiY1ETQ2OwE1NDYzMhYdASE1NDYzMhYdAQERFBYzITI2NREhBSImNTQ2MyEyFhUUBiMhFSImNTQ2MyEyFhUUBiMhAuAZEhEZ/uoZERIZoBIZA1YZEqCgNUtLNf0ANUtLNaAZEhEZARYZERIZ/XUZEgMAEhn8qgEAERkZEQFWERkZEf6qERkZEQFWERkZEf6qAxUqEhkZEioqEhkZEioZEaCgERlWSzX9VTVLSzUCqzVLKhIZGRIqKhIZGRIq/or+SxIZGRIBtdUZEhEZGRESGZUZERIZGRIRGQAAAAEAOv/AA8YDqwAiAAAbAR4BFREUBiMiJjURAyY2MyEyFgcDERQGIyImNRE0NjcTIaXDAwMZEREZ4AwYGANEGBgM4BkRERkDA8P9SgNY/qIFCgX+8REZGREBBAGSFSkpFf5u/g8SGBgSAfwFCgUBXgAIAAD/wAQAA8AAGwA3AEcAWABoAHkAiQCZAAAFMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWNyInLgEnJjU0Nz4BNzYzMhceARcWFRQHDgEHBgMiBhURFBY7ATI2NRE0JiMnMzIWFREUBisBIiY1ETQ2MyEzMhYdARQGKwEiJj0BNDYXIgYdARQWOwEyNj0BNCYrARMzMhYdARQGKwEiJj0BNDYXIgYdARQWOwEyNj0BNCYjAgBqXV6LKCgoKIteXWpqXV6LKCgoKIteXWpjV1iCJSYmJYJYV2NjV1iCJSYmJYJYVw0IDAwIXAgMDAhYVRchIRdVGCEhGP73ZhggIBhmFyEhFAkLCwltCAsLCG0DZhggIBhmFyEhFAkLCwltCAsLCEAoKIteXWpqXV6LKCgoKIteXWpqXV6LKCghJiWCWFdjY1dYgiUmJiWCWFdjY1dYgiUmAqULCf6cCQsLCQFkCQshIhj+phgiIhgBWhgiIhikGSIiGaQYIiEMCK4JDAwJrggM/uciGR4YIyMYHhkiIQ0KJAkODgkkCg0AAAEAAP/AA9QDvwBQAAABNDYzMhYVMRUUBicmJy4BJyYjIgcOAQcGFRQXHgEXFjMyNz4BNzY/AT4BMzIWFRQGBzEGBw4BBwYjIicuAScmNTQ3PgE3NjMyFx4BFxYfATUDixUPEBU5DRopKWc9PEJbUFB3IiMjIndQUFtAOztmKCkbAQURCw8VAgIgLzB2RUVLal1diygpKSiLXV1qPDc4ZSssIwEDUg8VFQ/LHQ0bOS8uRBISIiJ4T1BbW1BQdyIjEhFALC01AwkLFRAECARANDRLFRQoKIteXWpqXV2LKCgNDTAiIisBTQADAAD/wAQAA8AAGwA4AEwAAAUiJy4BJyY1NDc+ATc2MzIXHgEXFhUUBw4BBwYnMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWMxMzMhYVFAYrASImNRE0NjMyFh0BAgBqXV6LKCgoKIteXWpqXV6LKCgoKIteXWpYTk50ISIiIXROTlhYTk50ISIiIXROTlgVlhEZGRHAEhkZEhEZQCgoi15dampdXosoKCgoi15dampdXosoKFUiIXROTlhYTk50ISIiIXROTlhYTk50ISIBqxkSERkZEQEWERkZEesAAAIADP/AA/QDwAAYADEAAAUxIiYnASY0NzYyHwERNDYzMhYVERQGIzEBMTIWFwEWFAcGIi8BERQGIyImNRE0NjMxAUwJEAb+3wwNDSMM2BkSERkZEQFoCRAGASEMDQ0jDNgZEhEZGRFABgcBKwwkDAwN3wNBEhkZEvxWEhkEAAYH/tUMJAwMDd/8vxIZGRIDqhIZAAABAAH/xgRHA8AALgAAASIGBy4BIyIHDgEHBhUUFhcWFx4BFxYXHgEzMjY3Njc+ATc2Nz4BNTQnLgEnJiMDE0Z+Kyt+Rz84OFMZGDseLEVFhDIyBQgVCgoUCQUxMoVFRSsePBkYUzg4QAPAQzs7QxsaXD09RlN5L0RISHkoKAMHBwcHAygoeUhIRC95U0Y9PVwaGwAABAAA/8AD1wOoAAcADQARABUAACUhESMRIREjARc3JwcXNzMRIxMRIxEDhfzNUgPXUv5nrzrp6jqHUVFRURIBcf49AcMBsbA66uo6wf1xAo/9cQKPAAAAAAEACwCjA/UC3wATAAATLgEHDgEXARYyNwE2JicmBgcJAUoMIw0NAgwB1gwmDAHWDAINDSMM/kr+SgLdDQEMCyQN/gAODgIADSQLDAEN/iIB3gAAAAEA4//LAx8DtQATAAAlHgEHDgEnASY0NwE2FhcWBgcJAQMdDQEMCyQN/gAODgIADSQLDAEN/iIB3goMIw0NAgwB1gwmDAHWDAINDSMM/kr+SgAAAAEA4f/LAx0DtQATAAA3DgEXHgE3ATY0JwEmBgcGFhcJAeMNAQwLJA0CAA4O/gANJAsMAQ0B3v4iCgwjDQ0CDAHWDCYMAdYMAg0NIwz+Sv5KAAAAAAEACwChA/UC3QATAAA3DgEnLgE3ATYyFwEWBgcGJicJAUoMIw0NAgwB1gwmDAHWDAINDSMM/kr+SqMNAQwLJA0CAA4O/gANJAsMAQ0B3v4iAAAAAAEAUwA7A6QDKwBRAAABMQEGBwYUFxYXFhcWMjc2NwE2NCcmIgcBBhQXFjY3ATYyFxYUBzEBBiInJjQ3ATY3NjIXFhcWFxYUBwYHAQYHBiInJicmJyY0NzY3ATYyFx4BAdP+6B4QDw8QHh4mJ08mJh8BbCgoKXIp/qAUFBU6FAEMCRoJCQj+9CduJycnAWAdJSVNJiUdHg4PDw4e/pQnMjJnMjIoJxQUFBQnARgKGQkJAQLg/ugeJyZPJiceHg8QEA8eAWwocygpKf6hFToUFQEUAQwICQkZCv70JiYnbicBYB0PDw8PHR4lJU0lJR7+lSgUExMUKCcyMmcyMigBFwkJCRoAAQAM/80CiQPAABsAACURNDYzMhYVETc2MhceAQcBBiInASY0NzYyHwEBIBkSERnXDCQMDQEN/t8MJA3+4QwNDSMM11MDQhIZGRL8wN4NDAwkDP7VDQ0BKQwkDAwN3gAAAQC3/8ADNAOzABsAAAERFAYjIiY1EQcGIicuATcBNjIXARYUBwYiLwECIBkSERnXDCQMDQENASEMJA0BHwwNDSMM1wMt/L4SGRkSA0DeDQwMJAwBKw0N/tcMJAwMDd4AAQAMAHgD9AMJABMAAAE+ARcWFAcBBiInASY0NzYWFwkBA7cMIw0NDP27DSQM/poMDQ0jDAFHAicDCA0BDQwjDf2rDQ0Bbw0jDA0BDP6vAjYAAAAAAQA3//cDyQOJACAAAAkBJjQ3NjIXCQE2MhcWFAcJARYUBwYiJwkBBiInJjQ3AQHE/nMMDA0jDAGNAY0MIw0MDP5zAY0MDA0jDP5z/nMMIw0MDAGNAcABjQwjDQwM/nMBjQwMDSMM/nP+cwwjDQwMAY3+cwwMDSMMAY0AAAMAL//fA9EDggAUACkAWgAAEyIGFTERFBYzMSEyNjUxETQmIzEhNSEyFhUxERQGIzEhIiY1MRE0NjMxBRQGIyImNTE1NDYzITIWFREUBisBIiY1NDY7ATgBMTI2NRE0JiM4ATEhMCIxIgYdAYwNEhINAbINEhIN/k4Bsic2Nif+Tic2NicBFxINDRIwIgHJITAwIU0MExMMTQgLCwj+NwEICwJrEwz98A0SEg0CEAwTPjcm/fAmNzcmAhAmNw8MExMMlyEwMCH92CEwEg0NEgsIAigICwsIlwAAAAAJAAD/wAQAA8AAGwA3AEcAVwBoAHkAhQCRAJ0AAAUyNz4BNzY1NCcuAScmIyIHDgEHBhUUFx4BFxY3IicuAScmNTQ3PgE3NjMyFx4BFxYVFAcOAQcGEzI2PQE0JiMhIgYdARQWMyU1NCYjISIGHQE+ATMhMhYlITIWFREUBiMhIiY1ETQ2MxEiBh0BFBYzITI2PQE0JiMhNyImNTQ2MzIWFRQGByImNTQ2MzIWFRQGByImNTQ2MzIWFRQGAgBqXV6LKCgoKIteXWpqXV6LKCgoKIteXWpjV1iCJSYmJYJYV2NjV1iCJSYmJYJYV3QHCQkH/mMHCgoHAa0JB/5jBwoECAUBnQQI/lcBnRQdHRT+YxUdHRUHCgoHAZ0HCQkH/mNCDhMTDg4TEw4OExMODhMTDg4TEw4OExNAKCiLXl1qal1eiygoKCiLXl1qal1eiygoISYlglhXY2NXWIIlJiYlglhXY2NXWIIlJgGdCgZkBgoKBmQGCqJ3BwkJB3cCAQGmHRT+UhQdHRQBrhQd/pUKB2MHCQkHYwcK5xMODhMTDg4TpRMODhMTDg4TpRMODhMTDg4TAAAAAAQAAP/ABAADwAAgAFIAYQBwAAABERQGIyEiJjURNCYjIgYVMREUFjMhMjY1ETQmIyIGFTEDLgEjOAEjISIGDwEjIgYVFBYzMSEyNjcxNz4BMyEyFh8BHgEzOAExITI2NTQmIzEhJwERFBYzMjY1ETQmIyIGFSERFBYzMjY1ETQmIyIGFQOgLCD9eCAsEw0NE1I6Aog6UhMNDRPUBi8fAf73Hy8GCPINExMNAQ4MEgEMAQwIAQoHDAILAhIMASgNExMN/vQI/okTDQ4SEg4NEwEgEw0OEhIODRMCYP3rHywsHwIVDRMTDf3rOlFROgIVDRMTDQEaHigmHjwTDQ0TEAxWBQkJB1QMEBMNDRM6/s7+ZA4SEg4BnA4SEg7+ZA4SEg4BnA4SEg4AAgAA/8AESQPAADwAggAAASIHDgEHBhUUFhcWFx4BFxYXHgEfAR4BFx4BMzI2Nz4BNz4BNwc2Nz4BNzY3PgE1NCcuAScmIyIGBy4BIwUyFx4BFxYVFAYHBgcOAQcGBw4BDwEOAQ8BDgEjMTAiMSImJzEuAScXLgEnFyYnLgEnJicuATU0Nz4BNzYzMhYfATc+ATMBQ0M7O1caGSovExkZPCMkJxk1GwYRHQYMHhAPHg0FHREgNxoCJyQjPBkZEy8qGRpXOztDQHQtLXVAAcM0Li9FFBQjKxEYFzoiIiUYNBsFChcNBAMJBAEECAMQGgwDHjYaAiYhIjoXGBEqJBQURS8uNDpoJBscJWg5A8AcG2A/QEhHbUoeHx9DIiMjFy4WBQ4XBAoKCgoEFw8ZMBgCJCIjQh8fHkptR0hAP2AbHDQvLzRFFhZMMzQ6Ol1CGx4dPyEhIhYtFgQIEwoDAwMDAgwVCgIZLhgDIyEhPx0eG0JdOjo0M0wWFjYxJiUyNgAAAQAAAAEAAKb8EWdfDzz1AAsEAAAAAADZaF2HAAAAANloXYcAAP/ABEkDwAAAAAgAAgAAAAAAAAABAAADwP/AAAAESQAAAAAESQABAAAAAAAAAAAAAAAAAAAANAQAAAAAAAAAAAAAAAIAAAAEAABLBAAAAAQAACsEAAAABAAAAAQAAAAEAAArBAAAQAQAAAAEAAAABAAADgQAAAAEAAHVBAAAKwQAABUEAAAABAAAAAQAAAAEAAALBAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAOgQAAAAEAAAABAAAAAQAAAwESQABA+AAAAQAAAsEAADjBAAA4QQAAAsEAABTAqAADAQAALcEAAAMBAAANwQAAC8EAAAABAAAAARJAAAAAAAAAAoAFAAeAGQBSAGAAcICSALGAzQDhAPoBC4EiAUGBTIFTAV+BhQGggb2B5IIQAk8CfQK2gvcDVAOZA8+D8oQAhDSEUYRthIAEkoSdhKgEsoS9BMeE6AT0BQAFCoUaBTWFbIWRhcGAAAAAQAAADQBBwAKAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAYAAAABAAAAAAACAAcAVwABAAAAAAADAAYAMwABAAAAAAAEAAYAbAABAAAAAAAFAAsAEgABAAAAAAAGAAYARQABAAAAAAAKABoAfgADAAEECQABAAwABgADAAEECQACAA4AXgADAAEECQADAAwAOQADAAEECQAEAAwAcgADAAEECQAFABYAHQADAAEECQAGAAwASwADAAEECQAKADQAmG9uZXVpSQBvAG4AZQB1AGkASVZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMG9uZXVpSQBvAG4AZQB1AGkASW9uZXVpSQBvAG4AZQB1AGkASVJlZ3VsYXIAUgBlAGcAdQBsAGEAcm9uZXVpSQBvAG4AZQB1AGkASUZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=') - format('truetype'); - font-weight: normal; - font-style: normal; -} - -.new-fc-one-icon { - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'new-fc-one-icon' !important; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.new-fc-one-icon-arrow-to-top::before { - content: '\e900'; -} - -.new-fc-one-icon-bullseye::before { - content: '\e901'; -} - -.new-fc-one-icon-calendar::before { - content: '\e91b'; -} - -.new-fc-one-icon-time::before { - content: '\e91f'; -} - -.new-fc-one-icon-ellipsis::before { - content: '\e902'; -} - -.new-fc-one-icon-envelope::before { - content: '\e903'; -} - -.new-fc-one-icon-fail::before { - content: '\e904'; -} - -.new-fc-one-icon-warning::before { - content: '\e905'; -} - -.new-fc-one-icon-file::before { - content: '\e906'; -} - -.new-fc-one-icon-flag::before { - content: '\e907'; -} - -.new-fc-one-icon-folder-open::before { - content: '\e908'; -} - -.new-fc-one-icon-folder::before { - content: '\e909'; -} - -.new-fc-one-icon-home::before { - content: '\e90a'; -} - -.new-fc-one-icon-info::before { - content: '\e90b'; -} - -.new-fc-one-icon-loading::before { - content: '\e90c'; -} - -.new-fc-one-icon-minus::before { - content: '\e90d'; -} - -.new-fc-one-icon-plus::before { - content: '\e90e'; -} - -.new-fc-one-icon-question::before { - content: '\e90f'; -} - -.new-fc-one-icon-search::before { - content: '\e910'; -} - -.new-fc-one-icon-success::before { - content: '\e911'; -} - -.new-fc-one-icon-sync-alt::before { - content: '\e912'; -} - -.new-fc-one-icon-heart-active::before { - content: '\e921'; - color: #f85d5d; -} - -.new-fc-one-icon-recommend::before { - content: '\e918'; -} - -.new-fc-one-icon-material::before { - content: '\e91d'; -} - -.new-fc-one-icon-delete::before { - content: '\e931'; - color: #666; -} - -.new-fc-one-icon-heart-o::before { - content: '\e932'; - color: #999; -} - -.new-fc-one-icon-fund::before { - content: '\e917'; -} - -.new-fc-one-icon-volume::before { - content: '\e930'; -} - -.new-fc-one-icon-suggest::before { - content: '\e913'; -} - -.new-fc-one-icon-budget::before { - content: '\e914'; -} - -.new-fc-one-icon-verify::before { - content: '\e915'; -} - -.new-fc-one-icon-accumulate::before { - content: '\e916'; -} - -.new-fc-one-icon-cost::before { - content: '\e919'; -} - -.new-fc-one-icon-transform::before { - content: '\e91a'; -} - -.new-fc-one-icon-filter::before { - content: '\e91c'; -} - -.new-fc-one-icon-refresh::before { - content: '\e91e'; -} - -.new-fc-one-icon-sorting::before { - content: '\e920'; -} - -.new-fc-one-icon-upload::before { - content: '\e923'; -} - -.new-fc-one-icon-angle-down::before { - content: '\e925'; -} - -.new-fc-one-icon-angle-left::before { - content: '\e926'; -} - -.new-fc-one-icon-angle-right::before { - content: '\e927'; -} - -.new-fc-one-icon-angle-up::before { - content: '\e928'; -} - -.new-fc-one-icon-append::before { - content: '\e929'; -} - -.new-fc-one-icon-arrow-down::before { - content: '\e92a'; -} - -.new-fc-one-icon-arrow-up::before { - content: '\e92b'; -} - -.new-fc-one-icon-check::before { - content: '\e92d'; -} - -.new-fc-one-icon-close::before { - content: '\e92e'; -} - -.new-fc-one-icon-copy::before { - content: '\e92f'; -} - -.new-fc-one-select-another-xsmall { - font-size: 12px; - border-radius: 2px; -} - -.new-fc-one-select-another-xsmall .new-fc-one-select-another-selection { - border-radius: 2px; -} - -.new-fc-one-select-another-xsmall - .new-fc-one-select-another-selection-text-error { - font-size: 12px; -} - -.new-fc-one-select-another-xsmall - .new-fc-one-select-another-selection__rendered { - line-height: 22px; -} - -.new-fc-one-select-another-xsmall - .new-fc-one-select-another-selection--multiple { - min-height: 22px; -} - -.new-fc-one-select-another-xsmall - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__choice { - height: 16px; - line-height: 16px; - margin-top: 4px; - margin-bottom: 4px; -} - -.new-fc-one-select-another-xsmall - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__clear, -.new-fc-one-select-another-xsmall - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-arrow { - top: 12px; -} - -.new-fc-one-select-another-xsmall - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-search__field__wrap - input { - font-size: 12px; -} - -.new-fc-one-select-another-dropdown-xsmall - .new-fc-one-select-another-dropdown-menu-item { - height: 24px; - line-height: 24px; - font-size: 12px; -} - -.new-fc-one-select-another-dropdown-xsmall - .new-fc-one-select-another-dropdown-menu-item - .new-fc-one-checkbox-wrapper - + span { - font-size: 12px; -} - -.new-fc-one-select-another-dropdown-xsmall - .new-fc-one-select-another-dropdown-menu-item-group-title { - height: 24px; - line-height: 24px; - font-size: 12px; -} - -.new-fc-one-select-another-small { - font-size: 12px; - border-radius: 2px; -} - -.new-fc-one-select-another-small .new-fc-one-select-another-selection { - border-radius: 2px; -} - -.new-fc-one-select-another-small - .new-fc-one-select-another-selection-text-error { - font-size: 12px; -} - -.new-fc-one-select-another-small - .new-fc-one-select-another-selection__rendered { - line-height: 26px; -} - -.new-fc-one-select-another-small - .new-fc-one-select-another-selection--multiple { - min-height: 26px; -} - -.new-fc-one-select-another-small - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__choice { - height: 20px; - line-height: 20px; - margin-top: 4px; - margin-bottom: 4px; -} - -.new-fc-one-select-another-small - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__clear, -.new-fc-one-select-another-small - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-arrow { - top: 14px; -} - -.new-fc-one-select-another-small - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-search__field__wrap - input { - font-size: 12px; -} - -.new-fc-one-select-another-dropdown-small - .new-fc-one-select-another-dropdown-menu-item { - height: 28px; - line-height: 28px; - font-size: 12px; -} - -.new-fc-one-select-another-dropdown-small - .new-fc-one-select-another-dropdown-menu-item - .new-fc-one-checkbox-wrapper - + span { - font-size: 12px; -} - -.new-fc-one-select-another-dropdown-small - .new-fc-one-select-another-dropdown-menu-item-group-title { - height: 28px; - line-height: 28px; - font-size: 12px; -} - -.new-fc-one-select-another-medium { - font-size: 14px; - border-radius: 3px; -} - -.new-fc-one-select-another-medium .new-fc-one-select-another-selection { - border-radius: 3px; -} - -.new-fc-one-select-another-medium - .new-fc-one-select-another-selection-text-error { - font-size: 14px; -} - -.new-fc-one-select-another-medium - .new-fc-one-select-another-selection__rendered { - line-height: 30px; -} - -.new-fc-one-select-another-medium - .new-fc-one-select-another-selection--multiple { - min-height: 30px; -} - -.new-fc-one-select-another-medium - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__choice { - height: 24px; - line-height: 24px; - margin-top: 4px; - margin-bottom: 4px; -} - -.new-fc-one-select-another-medium - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__clear, -.new-fc-one-select-another-medium - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-arrow { - top: 16px; -} - -.new-fc-one-select-another-medium - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-search__field__wrap - input { - font-size: 14px; -} - -.new-fc-one-select-another-dropdown-medium - .new-fc-one-select-another-dropdown-menu-item { - height: 32px; - line-height: 32px; - font-size: 14px; -} - -.new-fc-one-select-another-dropdown-medium - .new-fc-one-select-another-dropdown-menu-item - .new-fc-one-checkbox-wrapper - + span { - font-size: 14px; -} - -.new-fc-one-select-another-dropdown-medium - .new-fc-one-select-another-dropdown-menu-item-group-title { - height: 32px; - line-height: 32px; - font-size: 14px; -} - -.new-fc-one-select-another-large { - font-size: 16px; - border-radius: 4px; -} - -.new-fc-one-select-another-large .new-fc-one-select-another-selection { - border-radius: 4px; -} - -.new-fc-one-select-another-large - .new-fc-one-select-another-selection-text-error { - font-size: 16px; -} - -.new-fc-one-select-another-large - .new-fc-one-select-another-selection__rendered { - line-height: 34px; -} - -.new-fc-one-select-another-large - .new-fc-one-select-another-selection--multiple { - min-height: 34px; -} - -.new-fc-one-select-another-large - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__choice { - height: 28px; - line-height: 28px; - margin-top: 4px; - margin-bottom: 4px; -} - -.new-fc-one-select-another-large - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__clear, -.new-fc-one-select-another-large - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-arrow { - top: 18px; -} - -.new-fc-one-select-another-large - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-search__field__wrap - input { - font-size: 16px; -} - -.new-fc-one-select-another-dropdown-large - .new-fc-one-select-another-dropdown-menu-item { - height: 36px; - line-height: 36px; - font-size: 16px; -} - -.new-fc-one-select-another-dropdown-large - .new-fc-one-select-another-dropdown-menu-item - .new-fc-one-checkbox-wrapper - + span { - font-size: 16px; -} - -.new-fc-one-select-another-dropdown-large - .new-fc-one-select-another-dropdown-menu-item-group-title { - height: 36px; - line-height: 36px; - font-size: 16px; -} - -.new-fc-one-select-another-disabled { - border: 1px solid #eee; - background-color: #fafafa; - color: #ccc; - cursor: not-allowed; -} - -.new-fc-one-select-another-disabled:hover, -.new-fc-one-select-another-disabled:active, -.new-fc-one-select-another-disabled:focus { - border-color: #eee; -} - -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection--multiple[type='list'] { - cursor: not-allowed; -} - -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice { - cursor: not-allowed; - background-color: #fafafa; - border: 1px solid #eee; -} - -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice:hover, -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice:active, -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice:focus { - background-color: #fafafa; -} - -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice__remove { - cursor: not-allowed; -} - -.new-fc-one-select-another:not(.new-fc-one-select-another-disabled) { - border: 1px solid #e0e0e0; - background-color: #fff; - color: #333; - cursor: pointer; -} - -.new-fc-one-select-another:not(.new-fc-one-select-another-disabled):hover, -.new-fc-one-select-another:not(.new-fc-one-select-another-disabled):active { - border-color: #999; -} - -.new-fc-one-select-another:not(.new-fc-one-select-another-disabled):focus { - border-color: #3d88f2; -} - -.new-fc-one-select-another { - line-height: 1; - box-sizing: border-box; - margin: 0; - padding: 0; - list-style: none; - display: inline-block; - position: relative; - outline: 0; - width: 120px; - padding: 0 12px; -} - -.new-fc-one-select-another-focused { - border-color: #3d88f2; -} - -.new-fc-one-select-another-open { - border-color: #999; -} - -.new-fc-one-select-another-open .new-fc-one-select-another-arrow { - transform: scale(0.7, 0.7) rotate(180deg); -} - -.new-fc-one-select-another ul, -.new-fc-one-select-another ol { - margin: 0; - padding: 0; - list-style: none; -} - -.new-fc-one-select-another > ul > li > a { - padding: 0; - background-color: #fff; -} - -.new-fc-one-select-another-error-line { - border: 1px solid #e64552 !important; -} - -.new-fc-one-select-another-error-line:hover, -.new-fc-one-select-another-error-line:focus, -.new-fc-one-select-another-error-line:active { - border-color: #e64552 !important; -} - -.new-fc-one-select-another-error-line:focus { - box-shadow: 0 0 0 2px rgba(230, 69, 82, 0.2); -} - -.new-fc-one-select-another-selection__clear, -.new-fc-one-select-another-custom-key, -.new-fc-one-select-another-arrow { - position: absolute; - top: 50%; - right: 0; - line-height: 1; - margin-top: -6px; - color: #666; - font-size: calc(1em - 4px); - transition: transform 0.3s; - width: 1em; - height: 1em; - background: #fff; -} - -.new-fc-one-select-another-arrow { - transform: scale(0.7, 0.7); -} - -.new-fc-one-select-another-selection__clear { - transform: scale(1); - color: #999; - z-index: 1; - opacity: 0; -} - -.new-fc-one-select-another-selection { - position: relative; - outline: none; - user-select: none; - box-sizing: border-box; - display: block; - border-radius: 0; - transition: all 0.3s ease-in-out; -} - -.new-fc-one-select-another-selection:hover - .new-fc-one-select-another-selection__clear { - opacity: 1; -} - -.new-fc-one-select-another-selection-selected-value { - float: left; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; -} - -.new-fc-one-select-another-selection-text-error { - color: #e64552; - margin-top: 4px; -} - -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection__clear { - display: none; - visibility: hidden; - pointer-events: none; -} - -.new-fc-one-select-another-disabled - .new-fc-one-select-another-selection--multiple - .new-fc-one-select-another-selection__choice__remove { - display: none; -} - -.new-fc-one-select-another-selection--single { - position: relative; -} - -.new-fc-one-select-another-selection__rendered { - display: block; - position: relative; - padding-right: 14px; -} - -.new-fc-one-select-another-selection__rendered::after { - content: '.'; - visibility: hidden; - pointer-events: none; - display: inline-block; - width: 0; - height: 0; -} - -.new-fc-one-select-another-selection__placeholder, -.new-fc-one-select-another-search__field__placeholder { - position: absolute; - left: 0; - right: 14px; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: left; - color: #999; -} - -.new-fc-one-select-another-search__field__placeholder { - left: 8px; -} - -.new-fc-one-select-another-search__field__mirror { - position: absolute; - top: -2px; - left: 0; - white-space: pre; - pointer-events: none; - opacity: 0; - color: #333; -} - -.new-fc-one-select-another-search--inline { - position: absolute; - height: 100%; - width: 100%; -} - -.new-fc-one-select-another-search--inline - .new-fc-one-select-another-search__field__wrap { - width: 100%; - height: 100%; -} - -.new-fc-one-select-another-search--inline - .new-fc-one-select-another-search__field { - border-width: 0; - height: 100%; - width: calc(100% - 12px); - background: transparent; - outline: 0; - border-radius: 0; - line-height: 1; - color: #333; -} - -.new-fc-one-select-another-search--inline > i { - float: right; -} - -.new-fc-one-select-another-selection--multiple[type='list'] { - cursor: text; - zoom: 1; - position: relative; -} - -.new-fc-one-select-another-selection--multiple[type='list']::before, -.new-fc-one-select-another-selection--multiple[type='list']::after { - content: ' '; - display: table; -} - -.new-fc-one-select-another-selection--multiple[type='list']::after { - clear: both; - visibility: hidden; - font-size: 0; - height: 0; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-search--inline { - float: left; - position: relative; - top: 1px; - left: 0; - width: auto; - padding: 0; - max-width: calc(100% - 12px); -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-search--inline - .new-fc-one-select-another-search__field { - max-width: 100%; - width: 0.75em; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-search-hidden { - height: 0; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-search-ul { - padding: 0; - margin: 0; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-search-ul::after { - content: ' '; - display: table; - clear: both; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__rendered { - height: auto; - padding-right: 40px; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__rendered::after { - display: none; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice { - color: #333; - background-color: #f5f5f5; - border-radius: 2px; - float: left; - margin-right: 4px; - max-width: 99%; - position: relative; - overflow: hidden; - transition: padding 0.3s ease-in-out; - padding: 0 26px 0 8px; - cursor: pointer; - border: 1px solid #e0e0e0; - box-sizing: border-box; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice:hover { - background-color: #eee; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice:focus { - background-color: #eee; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice:active { - background-color: #e0e0e0; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice__disabled { - cursor: not-allowed; - background-color: #fafafa; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice__content { - display: inline-block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - transition: margin 0.3s ease-in-out; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__choice__remove { - color: #666; - line-height: inherit; - cursor: pointer; - display: inline-block; - font-weight: bold; - transition: all 0.3s; - position: absolute; - right: 8px; - transform: scale(0.7, 0.7); - top: 1px; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__clear { - top: 16px; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__total_count { - right: 0; - position: absolute; - color: #999; - bottom: 4px; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__total_count-error { - color: #e64552; -} - -.new-fc-one-select-another-selection--multiple[type='list'] - .new-fc-one-select-another-selection__total_count-min { - right: 20px; - top: 50%; - transform: translateY(-50%); - height: 20px; - line-height: 20px; -} - -.new-fc-one-select-another-hidden { - display: none; -} - -.new-fc-one-select-another-multiple .new-fc-one-select-another { - width: 300px; -} - -.new-fc-one-select-another-dropdown { - line-height: 1; - margin: 0; - padding: 0; - list-style: none; - font-variant: initial; - background-color: #fff; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); - border-radius: 3px; - box-sizing: border-box; - z-index: 1051; - left: -9999px; - top: -9999px; - position: absolute; - outline: none; -} - -.new-fc-one-select-another-dropdown.slide-up-enter.slide-up-enter-active.new-fc-one-select-another-dropdown-placement-topLeft, -.new-fc-one-select-another-dropdown.slide-up-appear.slide-up-appear-active.new-fc-one-select-another-dropdown-placement-topLeft { - animation-name: oneUISlideDownInEffect; -} - -.new-fc-one-select-another-dropdown.slide-up-leave.slide-up-leave-active.new-fc-one-select-another-dropdown-placement-bottomLeft { - animation-name: oneUISlideUpOutEffect; -} - -.new-fc-one-select-another-dropdown.slide-up-leave.slide-up-leave-active.new-fc-one-select-another-dropdown-placement-topLeft { - animation-name: oneUISlideDownOutEffect; -} - -.new-fc-one-select-another-dropdown-hidden { - display: none; -} - -.new-fc-one-select-another-dropdown-menu { - outline: none; - margin: 0; - padding-left: 0; - list-style: none; - overflow: auto; -} - -.new-fc-one-select-another-dropdown-menu-item-group-list { - margin: 0; - padding: 0; -} - -.new-fc-one-select-another-dropdown-menu-item-group-list - > .new-fc-one-select-another-dropdown-menu-item { - padding-left: 8px; -} - -.new-fc-one-select-another-dropdown-menu-item-group-title { - color: #999; - padding: 0 8px; -} - -.new-fc-one-select-another-dropdown-menu-item-group-list - .new-fc-one-select-another-dropdown-menu-item:first-child:not(:last-child), -.new-fc-one-select-another-dropdown-menu-item-group:not(:last-child) - .new-fc-one-select-another-dropdown-menu-item-group-list - .new-fc-one-select-another-dropdown-menu-item:last-child { - border-radius: 0; -} - -.new-fc-one-select-another-dropdown-menu-item-group:not(:last-child) { - padding-bottom: 2px; - border-bottom: 1px solid #999; -} - -.new-fc-one-select-another-dropdown-menu-item[type='custom'] { - position: relative; - display: block; - padding: 0 8px; -} - -.new-fc-one-select-another-dropdown-menu-item { - position: relative; - display: block; - padding: 0 8px; - white-space: nowrap; - cursor: pointer; - overflow: hidden; - text-overflow: ellipsis; - transition: background 0.3s ease; - background-color: #fff; - color: #333; -} - -.new-fc-one-select-another-dropdown-menu-item:hover { - background-color: #f5f5f5; - color: #000; -} - -.new-fc-one-select-another-dropdown-menu-item:active { - background-color: #eee; - color: #000; -} - -.new-fc-one-select-another-dropdown-menu-item:focus { - background-color: #f2f7ff; - color: #000; -} - -.new-fc-one-select-another-dropdown-menu-item-selected { - background-color: #fff; - color: #3d88f2; -} - -.new-fc-one-select-another-dropdown-menu-item-selected:hover { - background-color: #f5f5f5; - color: #3d88f2; -} - -.new-fc-one-select-another-dropdown-menu-item-selected:active { - background-color: #eee; - color: #3d88f2; -} - -.new-fc-one-select-another-dropdown-menu-item-selected:focus { - background-color: #f2f7ff; - color: #3d88f2; -} - -.new-fc-one-select-another-dropdown-menu-item-divider { - height: 1px; - margin: 1px 0; - overflow: hidden; - background-color: #eee; - line-height: 0; -} - -.new-fc-one-select-another-dropdown-menu-item-disabled { - background-color: #fafafa; - color: #ccc; - cursor: not-allowed; -} - -.new-fc-one-select-another-dropdown-menu-item-disabled:hover { - background-color: #fafafa; - color: #ccc; -} - -.new-fc-one-select-another-dropdown-menu-item-disabled:active { - background-color: #fafafa; - color: #ccc; -} - -.new-fc-one-select-another-dropdown-menu-item-disabled:focus { - background-color: #fafafa; - color: #ccc; -} - -.new-fc-one-select-another-dropdown-menu-container { - width: 172px; - height: 152px; - overflow: auto; -} - -.new-fc-one-select-another-dropdown-menu .new-fc-one-checkbox-wrapper { - font-size: inherit; -} - -.new-fc-one-select-another-dropdown--multiple - .new-fc-one-select-another-dropdown-menu-item-selected - .new-fc-one-checkbox-wrapper - + span { - background-color: transparent; - color: #333; -} - -.new-fc-one-select-another-dropdown--multiple - .new-fc-one-select-another-dropdown-menu-item-selected - .new-fc-one-checkbox-wrapper - + span:hover { - background-color: transparent; - color: #000; -} - -.new-fc-one-select-another-dropdown--multiple - .new-fc-one-select-another-dropdown-menu-item-selected - .new-fc-one-checkbox-wrapper - + span:active { - background-color: transparent; - color: #000; -} - -.new-fc-one-select-another-dropdown--multiple - .new-fc-one-select-another-dropdown-menu-item-selected - .new-fc-one-checkbox-wrapper - + span:focus { - background-color: transparent; - color: #000; -} - -.new-fc-one-select-another-dropdown-xsmall - .new-fc-one-select-another-dropdown-menu { - max-height: 248px; -} - -.new-fc-one-select-another-dropdown-small - .new-fc-one-select-another-dropdown-menu { - max-height: 288px; -} - -.new-fc-one-select-another-dropdown-medium - .new-fc-one-select-another-dropdown-menu { - max-height: 328px; -} - -.new-fc-one-select-another-dropdown-large - .new-fc-one-select-another-dropdown-menu { - max-height: 368px; -} - -.new-fc-one-select-another-search-text-highlight { - color: #f27c49; -} - -.new-fc-one-single-select { - background: #fff; - cursor: pointer; - position: relative; - color: #333; - border: 1px solid #dbdbdb; - padding-right: 20px; - box-sizing: border-box; - height: 32px; - line-height: 1; - display: inline-block; - vertical-align: middle; -} - -.new-fc-one-single-select-text { - font-size: 12px; - padding: 0 10px; - display: inline-block; - line-height: 30px; - height: 30px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: 100%; -} - -.new-fc-one-single-select:hover { - border: 1px solid #999; -} - -.new-fc-one-select-another-focused .new-fc-one-single-select, -.new-fc-one-single-select:focus, -.new-fc-one-single-select:active { - border: 1px solid #999; -} - -.new-fc-one-single-select-open .new-fc-one-icon { - transform: rotate(180deg) scale(0.7); -} - -.new-fc-one-single-select-disabled { - background: #eee; - cursor: not-allowed; - color: #b8b8b8; -} - -.new-fc-one-single-select-disabled:hover { - border: 1px solid #dbdbdb; -} - -.new-fc-one-select-another-focused .new-fc-one-single-select-disabled, -.new-fc-one-single-select-disabled:focus, -.new-fc-one-single-select-disabled:active { - border: 1px solid #dbdbdb; -} - -.new-fc-one-multiple-select { - position: relative; -} - -.new-fc-one-multiple-select-open .new-fc-one-icon { - transform: rotate(180deg) scale(0.7); -} - -.new-fc-one-multiple-select-text-label { - display: inline-block; -} - -.new-fc-one-select-another-popover { - line-height: 1; - margin: 0; - padding: 0; - list-style: none; - padding: 8px; - font-variant: initial; - background-color: #fff; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); - border-radius: 0; - box-sizing: border-box; - z-index: 1051; - left: -9999px; - top: -9999px; - position: absolute; - outline: none; -} - -.new-fc-one-select-another-popover.slide-up-enter.slide-up-enter-active.new-fc-one-select-another-popover-placement-topLeft, -.new-fc-one-select-another-popover.slide-up-appear.slide-up-appear-active.new-fc-one-select-another-popover-placement-topLeft { - animation-name: oneUISlideDownInEffect; -} - -.new-fc-one-select-another-popover.slide-up-leave.slide-up-leave-active.new-fc-one-select-another-popover-placement-bottomLeft { - animation-name: oneUISlideUpOutEffect; -} - -.new-fc-one-select-another-popover.slide-up-leave.slide-up-leave-active.new-fc-one-select-another-popover-placement-topLeft { - animation-name: oneUISlideDownOutEffect; -} - -.new-fc-one-select-another-popover-hidden { - display: none; -} - -.new-fc-one-select-another-popover-container { - width: 288px; -} - -.new-fc-one-select-another-popover-container-open - .new-fc-one-select-another-arrow { - transform: scale(0.7, 0.7) rotate(180deg); -} - -.new-fc-one-select-another-popover-container - .new-fc-one-select-another-selection__total { - top: 0 !important; - left: 8px !important; -} - -.new-fc-one-select-another-popover-container - .new-fc-one-select-another-selection { - cursor: auto; -} - -.new-fc-one-select-another-popover-inner { - width: 100%; -} - -.new-fc-one-select-another-popover-inner-container { - display: inline-block; - width: 100%; -} - -.new-fc-one-select-another-popover-inner-container-custom { - margin-bottom: 8px; -} - -.new-fc-one-select-another-popover-inner-container-button-item { - margin-right: 8px; -} - -.new-fc-one-select-another-pop { - line-height: 1; - margin: 0; - padding: 0; - list-style: none; - font-variant: initial; - background-color: #fff; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); - border-radius: 0; - box-sizing: border-box; - z-index: 1051; - left: -9999px; - top: -9999px; - position: absolute; - outline: none; -} - -.new-fc-one-select-another-pop.slide-up-enter.slide-up-enter-active.new-fc-one-select-another-pop-placement-topLeft, -.new-fc-one-select-another-pop.slide-up-appear.slide-up-appear-active.new-fc-one-select-another-pop-placement-topLeft { - animation-name: oneUISlideDownInEffect; -} - -.new-fc-one-select-another-pop.slide-up-leave.slide-up-leave-active.new-fc-one-select-another-pop-placement-bottomLeft { - animation-name: oneUISlideUpOutEffect; -} - -.new-fc-one-select-another-pop.slide-up-leave.slide-up-leave-active.new-fc-one-select-another-pop-placement-topLeft { - animation-name: oneUISlideDownOutEffect; -} - -.new-fc-one-select-another-pop-hidden { - display: none; -} - -.new-fc-one-select-another-container { - display: inline-block; -} - -.new-fc-one-select-another-selection-item { - position: relative; - top: -10px; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/NewVertexType.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/NewVertexType.tsx deleted file mode 100644 index 7bf17bd6d..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/NewVertexType.tsx +++ /dev/null @@ -1,1023 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { cloneDeep, isUndefined } from 'lodash-es'; -import { - Input, - Radio, - Select, - Button, - Switch, - Tooltip, - Checkbox, - Message -} from 'hubble-ui'; -import { useTranslation } from 'react-i18next'; - -import { Tooltip as CustomTooltip } from '../../../common/'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { formatVertexIdText } from '../../../../stores/utils'; - -import type { VertexTypeValidatePropertyIndexes } from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import HintIcon from '../../../../assets/imgs/ic_question_mark.svg'; -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import closeIcon from '../../../../assets/imgs/ic_close_16.svg'; - -import './NewVertexType.less'; - -const NewVertexType: React.FC = observer(() => { - const dataAnalyzeStore = useContext(DataAnalyzeStore); - const { metadataPropertyStore, vertexTypeStore } = useContext( - MetadataConfigsRootStore - ); - const { t } = useTranslation(); - const [isAddNewProperty, switchIsAddNewProperty] = useState(false); - const [deletePopIndex, setDeletePopIndex] = useState(null); - const deleteWrapperRef = useRef(null); - const dropdownWrapperRef = useRef(null); - - // need useCallback to stop infinite callings of useEffect - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - // if clicked element is not on dropdown, collpase it - if ( - isAddNewProperty && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddNewProperty(false); - } - - if ( - deletePopIndex && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - setDeletePopIndex(null); - } - }, - [deletePopIndex, isAddNewProperty] - ); - - useEffect(() => { - metadataPropertyStore.fetchMetadataPropertyList({ fetchAll: true }); - vertexTypeStore.validateAllNewVertexType(true); - }, [metadataPropertyStore, vertexTypeStore]); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( -
    -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - * - {t('addition.vertex.vertex-type-name')}: -
    - { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - name: e.value - }); - }} - originInputProps={{ - onBlur() { - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType('name'); - } - }} - /> -
    - -
    -
    - * - {t('addition.vertex.vertex-style')}: -
    -
    - -
    -
    - -
    -
    - -
    -
    - * - {t('addition.common.id-strategy')}: -
    - ) => { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - id_strategy: e.target.value - }); - - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType('primaryKeys'); - }} - > - - {t('addition.constant.primary-key-id')} - - - {t('addition.constant.automatic-generation')} - - - {t('addition.constant.custom-string')} - - - {t('addition.constant.custom-number')} - - - {t('addition.constant.custom-uuid')} - - -
    -
    -
    - {vertexTypeStore.newVertexType.id_strategy === 'PRIMARY_KEY' && ( - * - )} - {t('addition.common.association-property')}: -
    -
    - {vertexTypeStore.newVertexType.properties.length !== 0 && ( -
    -
    -
    {t('addition.common.property')}
    -
    {t('addition.common.allow-null')}
    -
    - {vertexTypeStore.newVertexType.properties.map( - (property, index) => { - const currentProperties = cloneDeep( - vertexTypeStore.newVertexType.properties - ); - - return ( -
    -
    {property.name}
    -
    - { - currentProperties[index].nullable = checked; - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - properties: currentProperties - }); - - // remove primary keys since it could be empty value - if (checked) { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - primary_keys: - vertexTypeStore.newVertexType.primary_keys.filter( - (key) => key !== property.name - ) - }); - } - }} - size="large" - /> -
    -
    - ); - } - )} -
    - )} -
    { - switchIsAddNewProperty(!isAddNewProperty); - }} - > - {t('addition.common.add-property')} - toggleAddProperty -
    -
    -
    - {isAddNewProperty && ( -
    -
    -
    - {metadataPropertyStore.metadataProperties.map((property) => ( -
    - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesInSelectedVertextType = - vertexTypeStore.addedPropertiesInSelectedVertextType; - - addedPropertiesInSelectedVertextType.has(property.name) - ? addedPropertiesInSelectedVertextType.delete( - property.name - ) - : addedPropertiesInSelectedVertextType.add( - property.name - ); - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - properties: [ - ...addedPropertiesInSelectedVertextType - ].map((propertyName) => { - const currentProperty = - vertexTypeStore.newVertexType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined(currentProperty) - ? currentProperty.nullable - : true - }; - }) - }); - - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType('properties'); - }} - > - {property.name} - - -
    - ))} -
    -
    - )} - {vertexTypeStore.newVertexType.id_strategy === 'PRIMARY_KEY' && ( -
    -
    - * - {t('addition.common.primary-key-property')}: -
    - -
    - )} - -
    -
    - * - {t('addition.vertex.vertex-display-content')}: -
    - -
    - -
    - - {t('addition.edge.index-info')} - - - hint - -
    -
    -
    - * - {t('addition.menu.type-index')}: -
    - { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - open_label_index: - !vertexTypeStore.newVertexType.open_label_index - }); - }} - size="large" - /> -
    -
    -
    - {t('addition.common.property-index')}: -
    -
    - {vertexTypeStore.newVertexType.property_indexes.length !== 0 && ( -
    -
    - {t('addition.edge.index-name')} -
    -
    - {t('addition.edge.index-type')} -
    -
    {t('addition.common.property')}
    -
    - )} - {vertexTypeStore.newVertexType.property_indexes.map( - ({ name, type, fields }, index) => ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - vertexTypeStore.newVertexType.property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType( - 'propertyIndexes' - ); - } - }} - /> -
    -
    - -
    -
    - -
    - - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const propertyIndexEntities = cloneDeep( - vertexTypeStore.newVertexType.property_indexes - ); - - propertyIndexEntities.splice(index, 1); - - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - property_indexes: propertyIndexEntities - }); - - vertexTypeStore.validateAllNewVertexType(true); - vertexTypeStore.validateNewVertexType( - 'propertyIndexes' - ); - - setDeletePopIndex(null); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeletePopIndex(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: closeIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeletePopIndex(index); - } - }} - childrenWrapperElement="img" - /> -
    - ) - )} - { - if ( - vertexTypeStore.newVertexType.property_indexes.length === 0 || - vertexTypeStore.isAddNewPropertyIndexReady - ) { - vertexTypeStore.mutateNewProperty({ - ...vertexTypeStore.newVertexType, - property_indexes: [ - ...vertexTypeStore.newVertexType.property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - vertexTypeStore.validateAllNewVertexType(true); - // set isAddNewPropertyIndexReady to false - vertexTypeStore.validateNewVertexType( - 'propertyIndexes', - true - ); - } - }} - style={{ - cursor: 'pointer', - color: vertexTypeStore.isAddNewPropertyIndexReady - ? '#2b65ff' - : '#999', - lineHeight: '32px' - }} - > - {t('addition.edge.add-group')} - -
    -
    - -
    -
    - - -
    -
    - - ); -}); - -export default NewVertexType; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/ReuseVertexTypes.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/ReuseVertexTypes.less deleted file mode 100644 index 71698137e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/ReuseVertexTypes.less +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.reuse-properties-wrapper { - width: 100%; - background: #fff; - margin-top: 16px; - padding: 16px; - - .reuse-steps { - width: 860px; - margin: 16px auto 18px; - } - - .reuse-properties { - &-row { - display: flex; - align-items: center; - margin: 18px 0 32px; - font-size: 14px; - color: #333; - - &:last-child { - margin-bottom: 24px; - } - - &-name { - width: 98px; - text-align: right; - margin-right: 13.9px; - } - } - - &-manipulations { - display: flex; - justify-content: center; - margin-bottom: 88px; - } - - &-validate { - &-duplicate, - &-exist, - &-pass { - width: 58px; - margin: 0 auto; - border-radius: 2px; - letter-spacing: 0; - line-height: 22px; - text-align: center; - } - - &-duplicate { - background: #fff2f2; - border: 1px solid #ff9499; - color: #e64552; - } - - &-exist, - &-pass { - background: #f2fff4; - border: 1px solid #7ed988; - color: #39bf45; - } - } - - &-complete-hint { - display: flex; - flex-direction: column; - justify-content: center; - margin: 88px auto 327px; - - &-description { - display: flex; - justify-content: center; - - & > div { - margin-left: 20px; - } - } - - &-manipulations { - margin: 42px auto 0; - } - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/ReuseVertexTypes.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/ReuseVertexTypes.tsx deleted file mode 100644 index ba1540f2f..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/ReuseVertexTypes.tsx +++ /dev/null @@ -1,1052 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { useContext, useState, useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { - Select, - Steps, - Transfer, - Button, - Table, - Input, - Message -} from 'hubble-ui'; - -import { GraphManagementStoreContext } from '../../../../stores'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; - -import PassIcon from '../../../../assets/imgs/ic_pass.svg'; - -import './ReuseVertexTypes.less'; -import { cloneDeep } from 'lodash-es'; -import { useTranslation } from 'react-i18next'; - -const ReuseVertexTypes: React.FC = observer(() => { - const graphManagementStore = useContext(GraphManagementStoreContext); - const metadataConfigsRootStore = useContext(MetadataConfigsRootStore); - const { vertexTypeStore } = metadataConfigsRootStore; - const { t } = useTranslation(); - const [currentStatus, setCurrentStatus] = useState(1); - // acutally the name, not id in database - const [selectedId, mutateSelectedId] = useState<[] | string>([]); - const [selectedList, mutateSelectedList] = useState([]); - - // step 2 - const [vertexTypeEditIndex, setVertexTypeEditIndex] = useState( - null - ); - const [propertyEditIndex, setPropertyEditIndex] = useState( - null - ); - const [propertyIndexEditIndex, setPropertyIndexEditIndex] = useState< - number | null - >(null); - - // hack: need to call @observable at here to dispatch re-render by mobx - // since @action in onBlur() in doesn't dispatch re-render - vertexTypeStore.validateReuseErrorMessage.vertexType.toUpperCase(); - vertexTypeStore.validateReuseErrorMessage.property.toUpperCase(); - vertexTypeStore.validateReuseErrorMessage.property_index.toUpperCase(); - - const vertexTypeColumnConfigs = [ - { - title: t('addition.common.vertex-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (index !== vertexTypeEditIndex) { - return ( -
    - {text} -
    - ); - } - - return ( - { - // remove validate message after user input changes - vertexTypeStore.resetValidateReuseErrorMessage('vertexType'); - - const editedCheckedReusableData = cloneDeep( - vertexTypeStore.editedCheckedReusableData! - ); - - editedCheckedReusableData.vertexlabel_conflicts[ - index - ].entity.name = e.value; - - vertexTypeStore.mutateEditedReusableData( - editedCheckedReusableData - ); - }} - originInputProps={{ - onBlur() { - vertexTypeStore.validateReuseData( - 'vertexType', - vertexTypeStore.checkedReusableData!.vertexlabel_conflicts[ - index - ].entity.name, - vertexTypeStore.editedCheckedReusableData! - .vertexlabel_conflicts[index].entity.name - ); - } - }} - /> - ); - } - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - width: '30%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if (vertexTypeStore.reusableVertexTypeNameChangeIndexes.has(index)) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'DEP_CONFLICT': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.dependency-conflict'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return ( -
    - {text} -
    - ); - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - if (index === vertexTypeEditIndex) { - const originalName = vertexTypeStore.checkedReusableData! - .vertexlabel_conflicts[index].entity.name; - const changedName = vertexTypeStore.editedCheckedReusableData! - .vertexlabel_conflicts[index].entity.name; - const isChanged = changedName !== originalName; - - return ( -
    - { - if ( - !isChanged || - !vertexTypeStore.validateReuseData( - 'vertexType', - originalName, - changedName - ) - ) { - return; - } - - vertexTypeStore.mutateReuseData( - 'vertexType', - originalName, - changedName - ); - setVertexTypeEditIndex(null); - vertexTypeStore.mutateReusableVertexTypeChangeIndexes(index); - }} - > - {t('addition.common.save')} - - { - vertexTypeStore.resetValidateReuseErrorMessage('vertexType'); - setVertexTypeEditIndex(null); - vertexTypeStore.resetEditedReusableVertexTypeName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (vertexTypeEditIndex !== null) { - return; - } - - setVertexTypeEditIndex(index); - }} - > - {t('addition.operate.rename')} - - { - if (vertexTypeEditIndex !== null) { - return; - } - - setPropertyEditIndex(null); - - // remove selected status of the property in - const newSelectedList = [...selectedList].filter( - (property) => - property !== - vertexTypeStore.editedCheckedReusableData! - .vertexlabel_conflicts[index].entity.name - ); - - mutateSelectedList(newSelectedList); - - // notice: useState hooks cannot sync updated state value, so the length is still 1 - if (selectedList.length === 1) { - setCurrentStatus(1); - // remove edit status after return previous - vertexTypeStore.clearReusableNameChangeIndexes(); - return; - } - - vertexTypeStore.deleteReuseData('vertexlabel_conflicts', index); - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - const metadataPropertyColumnConfigs = [ - { - title: t('addition.common.property-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (index !== propertyEditIndex) { - return text; - } - - return ( - { - // remove validate message after user input changes - vertexTypeStore.resetValidateReuseErrorMessage('property'); - - const editedCheckedReusableData = cloneDeep( - vertexTypeStore.editedCheckedReusableData! - ); - - editedCheckedReusableData.propertykey_conflicts[ - index - ].entity.name = e.value; - - vertexTypeStore.mutateEditedReusableData( - editedCheckedReusableData - ); - }} - originInputProps={{ - onBlur() { - vertexTypeStore.validateReuseData( - 'property', - vertexTypeStore.checkedReusableData!.propertykey_conflicts[ - index - ].entity.name, - vertexTypeStore.editedCheckedReusableData! - .propertykey_conflicts[index].entity.name - ); - } - }} - /> - ); - } - }, - { - title: t('addition.common.data-type'), - dataIndex: 'data_type', - width: '15%', - render(text: string) { - if (text === 'TEXT') { - return 'string'; - } - - return text.toLowerCase(); - } - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - align: 'center', - width: '15%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if (vertexTypeStore.reusablePropertyNameChangeIndexes.has(index)) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'DEP_CONFLICT': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.dependency-conflict'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return
    {text}
    ; - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - if (index === propertyEditIndex) { - const originalName = vertexTypeStore.checkedReusableData! - .propertykey_conflicts[index].entity.name; - const changedName = vertexTypeStore.editedCheckedReusableData! - .propertykey_conflicts[index].entity.name; - const isChanged = changedName !== originalName; - - return ( -
    - { - if ( - !isChanged || - !vertexTypeStore.validateReuseData( - 'property', - originalName, - changedName - ) - ) { - return; - } - - vertexTypeStore.mutateReuseData( - 'property', - originalName, - changedName - ); - - setPropertyEditIndex(null); - vertexTypeStore.mutateReusablePropertyNameChangeIndexes( - index - ); - }} - > - {t('addition.common.save')} - - { - vertexTypeStore.resetValidateReuseErrorMessage('property'); - setPropertyEditIndex(null); - vertexTypeStore.resetEditedReusablePropertyName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (propertyEditIndex !== null) { - return; - } - - setPropertyEditIndex(index); - }} - > - {t('addition.operate.rename')} - - { - if (propertyEditIndex !== null) { - return; - } - - setPropertyEditIndex(null); - - vertexTypeStore.deleteReuseData('propertykey_conflicts', index); - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - const metadataPropertyIndexColumnConfigs = [ - { - title: t('addition.common.property-index-name'), - dataIndex: 'name', - width: '50%', - render(text: string, records: any, index: number) { - if (index !== propertyIndexEditIndex) { - return text; - } - - return ( - { - vertexTypeStore.resetValidateReuseErrorMessage('property_index'); - - const editedCheckedReusableData = cloneDeep( - vertexTypeStore.editedCheckedReusableData! - ); - - editedCheckedReusableData.propertyindex_conflicts[ - index - ].entity.name = e.value; - - vertexTypeStore.mutateEditedReusableData( - editedCheckedReusableData - ); - }} - originInputProps={{ - onBlur() { - vertexTypeStore.validateReuseData( - 'property_index', - vertexTypeStore.checkedReusableData!.propertyindex_conflicts[ - index - ].entity.name, - vertexTypeStore.editedCheckedReusableData! - .propertyindex_conflicts[index].entity.name - ); - } - }} - /> - ); - } - }, - { - title: t('addition.vertex.corresponding-vertex-type'), - dataIndex: 'owner', - width: '15%' - }, - { - title: t('addition.edge.verification-result'), - dataIndex: 'status', - align: 'center', - width: '15%', - render(value: string, records: any, index: number) { - let classname = ''; - let text = ''; - - if (vertexTypeStore.reusablePropertyIndexNameChangeIndexes.has(index)) { - return ( -
    - {t('addition.edge.be-verified')} -
    - ); - } - - switch (value) { - case 'DUPNAME': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.duplicate-name'); - break; - - case 'DEP_CONFLICT': - classname = 'reuse-properties-validate-duplicate'; - text = t('addition.message.dependency-conflict'); - break; - - case 'EXISTED': - classname = 'reuse-properties-validate-exist'; - text = t('addition.message.already-exist'); - break; - - case 'PASSED': - classname = 'reuse-properties-validate-pass'; - text = t('addition.message.pass'); - break; - } - - return
    {text}
    ; - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '20%', - render(_: never, records: any, index: number) { - if (index === propertyIndexEditIndex) { - const originalName = vertexTypeStore.checkedReusableData! - .propertyindex_conflicts[index].entity.name; - const changedName = vertexTypeStore.editedCheckedReusableData! - .propertyindex_conflicts[index].entity.name; - const isChanged = changedName !== originalName; - - return ( -
    - { - if ( - !isChanged || - !vertexTypeStore.validateReuseData( - 'property_index', - originalName, - changedName - ) - ) { - return; - } - - vertexTypeStore.mutateReuseData( - 'property_index', - originalName, - changedName - ); - - setPropertyIndexEditIndex(null); - vertexTypeStore.mutateReusablePropertyIndexNameChangeIndexes( - index - ); - }} - > - {t('addition.common.save')} - - { - vertexTypeStore.resetValidateReuseErrorMessage( - 'property_index' - ); - setPropertyIndexEditIndex(null); - vertexTypeStore.resetEditedReusablePropertyIndexName(index); - }} - > - {t('addition.common.cancel')} - -
    - ); - } - - return ( -
    - { - if (propertyIndexEditIndex !== null) { - return; - } - - setPropertyIndexEditIndex(index); - }} - > - {t('addition.operate.rename')} - - { - if (propertyIndexEditIndex !== null) { - return; - } - - setPropertyIndexEditIndex(null); - - vertexTypeStore.deleteReuseData( - 'propertyindex_conflicts', - index - ); - }} - > - {t('addition.common.del')} - -
    - ); - } - } - ]; - - useEffect(() => { - // unlike metadata properties, all vertex types only needs here(in reuse) - vertexTypeStore.fetchVertexTypeList({ fetchAll: true }); - }, [vertexTypeStore]); - - return ( -
    -
    - {t('addition.operate.reuse-vertex-type')} -
    -
    - {t('addition.message.reuse-vertex-type-notice')} -
    -
    - - {[ - t('addition.menu.select-reuse-item'), - t('addition.menu.confirm-reuse-item'), - t('addition.menu.complete-reuse') - ].map((title: string, index: number) => ( - index + 1 - ? 'finish' - : 'wait' - } - key={title} - /> - ))} - - - {currentStatus === 1 && ( - <> -
    -
    - * - {t('addition.newGraphConfig.id')}: -
    - -
    -
    -
    - * - {t('addition.operate.reuse-vertex-type')}: -
    - name - )} - selectedList={selectedList} - showSearchBox={false} - candidateTreeStyle={{ - width: 355, - fontSize: 14 - }} - selectedTreeStyle={{ - width: 355, - fontSize: 14 - }} - handleSelect={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleSelectAll={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleDelete={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - handleDeleteAll={(selectedList: string[]) => { - mutateSelectedList(selectedList); - }} - /> -
    -
    - - -
    - - )} - - {currentStatus === 2 && ( - <> -
    - {t('addition.common.selected-vertex-type')} -
    -
    ({ - name: entity.name, - status - }) - ) - : [] - } - pagination={false} - /> - -
    - {t('addition.common.selected-property')} -
    -
    ({ - name: entity.name, - data_type: entity.data_type, - status - }) - ) - : [] - } - pagination={false} - /> - -
    - {t('addition.common.selected-property-index')} -
    -
    ({ - name: entity.name, - owner: entity.owner, - status - }) - ) - : [] - } - pagination={false} - /> - -
    - - -
    - - )} - - {currentStatus === 3 && ( -
    -
    - {t('addition.message.reuse-complete')} -
    -
    {t('addition.message.reuse-complete')}
    -
    {t('addition.message.vertex-type-reuse-success')}
    -
    -
    -
    - - -
    -
    - )} - - - ); -}); - -export default ReuseVertexTypes; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/VertexTypeList.less b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/VertexTypeList.less deleted file mode 100644 index 20b083faa..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/VertexTypeList.less +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -.vertex-type-list-wrapper { - width: 100%; - background: #fff; - margin-top: 16px; - padding: 16px; -} diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/VertexTypeList.tsx b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/VertexTypeList.tsx deleted file mode 100644 index eaa7197c6..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/VertexTypeList.tsx +++ /dev/null @@ -1,1888 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback -} from 'react'; -import { observer } from 'mobx-react'; -import { - cloneDeep, - intersection, - size, - without, - isEmpty, - isUndefined, - values -} from 'lodash-es'; -import { useLocation } from 'wouter'; -import classnames from 'classnames'; -import { motion } from 'framer-motion'; -import { - Button, - Table, - Switch, - Modal, - Drawer, - Input, - Select, - Checkbox, - Message, - Loading -} from 'hubble-ui'; - -import { Tooltip, LoadingDataView } from '../../../common'; -import NewVertexType from './NewVertexType'; -import ReuseVertexTypes from './ReuseVertexTypes'; -import MetadataConfigsRootStore from '../../../../stores/GraphManagementStore/metadataConfigsStore/metadataConfigsStore'; -import DataAnalyzeStore from '../../../../stores/GraphManagementStore/dataAnalyzeStore/dataAnalyzeStore'; -import { formatVertexIdText } from '../../../../stores/utils'; - -import type { - VertexTypeValidatePropertyIndexes, - VertexType -} from '../../../../stores/types/GraphManagementStore/metadataConfigsStore'; - -import AddIcon from '../../../../assets/imgs/ic_add.svg'; -import BlueArrowIcon from '../../../../assets/imgs/ic_arrow_blue.svg'; -import WhiteCloseIcon from '../../../../assets/imgs/ic_close_white.svg'; -import CloseIcon from '../../../../assets/imgs/ic_close_16.svg'; -import i18next from '../../../../i18n'; -import { useTranslation } from 'react-i18next'; - -import './VertexTypeList.less'; - -const styles = { - button: { - marginLeft: 12, - width: 78 - }, - header: { - marginBottom: 16 - }, - manipulation: { - marginRight: 12 - }, - deleteWrapper: { - display: 'flex', - justifyContent: 'flex-end' - }, - loading: { - padding: 0, - marginRight: 4 - } -}; - -const variants = { - initial: { - opacity: 0 - }, - animate: { - opacity: 1, - transition: { - duration: 0.7 - } - }, - exit: { - opacity: 0, - transition: { - duration: 0.3 - } - } -}; - -const IDStrategyMappings: Record = { - PRIMARY_KEY: i18next.t('addition.constant.primary-key-id'), - AUTOMATIC: i18next.t('addition.constant.automatic-generation'), - CUSTOMIZE_STRING: i18next.t('addition.constant.custom-string'), - CUSTOMIZE_NUMBER: i18next.t('addition.constant.custom-number'), - CUSTOMIZE_UUID: i18next.t('addition.constant.custom-uuid') -}; - -const propertyIndexTypeMappings: Record = { - SECONDARY: i18next.t('addition.menu.secondary-index'), - RANGE: i18next.t('addition.menu.range-index'), - SEARCH: i18next.t('addition.menu.full-text-index') -}; - -const VertexTypeList: React.FC = observer(() => { - const metadataConfigsRootStore = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - const { metadataPropertyStore, vertexTypeStore } = metadataConfigsRootStore; - const [preLoading, switchPreLoading] = useState(true); - const [sortOrder, setSortOrder] = useState(''); - const [selectedRowKeys, mutateSelectedRowKeys] = useState([]); - const [isShowModal, switchShowModal] = useState(false); - const [isAddProperty, switchIsAddProperty] = useState(false); - const [isEditVertex, switchIsEditVertex] = useState(false); - const [ - deleteExistPopIndexInDrawer, - setDeleteExistPopIndexInDrawer - ] = useState(null); - const [ - deleteAddedPopIndexInDrawer, - setDeleteAddedPopIndexInDrawer - ] = useState(null); - const [, setLocation] = useLocation(); - - const dropdownWrapperRef = useRef(null); - const deleteWrapperInDrawerRef = useRef(null); - - const isLoading = - preLoading || - vertexTypeStore.requestStatus.fetchVertexTypeList === 'pending'; - - const currentSelectedRowKeys = intersection( - selectedRowKeys, - vertexTypeStore.vertexTypes.map(({ name }) => name) - ); - - // need useCallback to stop infinite callings of useEffect - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - // if clicked element is not on dropdown, collpase it - if ( - isEditVertex && - isAddProperty && - dropdownWrapperRef.current && - !dropdownWrapperRef.current.contains(e.target as Element) - ) { - switchIsAddProperty(false); - } - - if ( - (deleteExistPopIndexInDrawer || deleteAddedPopIndexInDrawer) && - deleteWrapperInDrawerRef.current && - !deleteWrapperInDrawerRef.current.contains(e.target as Element) - ) { - setDeleteExistPopIndexInDrawer(null); - setDeleteAddedPopIndexInDrawer(null); - } - }, - [deleteExistPopIndexInDrawer, deleteAddedPopIndexInDrawer, isAddProperty] - ); - - const handleSelectedTableRow = (newSelectedRowKeys: string[]) => { - mutateSelectedRowKeys(newSelectedRowKeys); - }; - - const handleCloseDrawer = () => { - switchIsAddProperty(false); - switchIsEditVertex(false); - vertexTypeStore.selectVertexType(null); - // clear mutations in - vertexTypeStore.resetEditedSelectedVertexType(); - }; - - const handleSortClick = () => { - switchPreLoading(true); - - if (sortOrder === 'descend') { - vertexTypeStore.mutatePageSort('asc'); - setSortOrder('ascend'); - } else { - vertexTypeStore.mutatePageSort('desc'); - setSortOrder('descend'); - } - - vertexTypeStore.fetchVertexTypeList(); - }; - - const batchDeleteProperties = async () => { - if ( - values(currentSelectedRowKeys).every( - (key) => vertexTypeStore.vertexTypeUsingStatus?.[key] - ) - ) { - Message.error({ - content: t('addition.message.no-can-delete-vertex-type'), - size: 'medium', - showCloseIcon: false - }); - - return; - } - - switchShowModal(false); - // need to set a copy in store since local row key state would be cleared - vertexTypeStore.mutateSelectedVertexTypeNames(currentSelectedRowKeys); - // mutateSelectedRowKeys([]); - await vertexTypeStore.deleteVertexType(currentSelectedRowKeys); - - if (vertexTypeStore.requestStatus.deleteVertexType === 'success') { - Message.success({ - content: t('addition.common.del-success'), - size: 'medium', - showCloseIcon: false - }); - - mutateSelectedRowKeys( - without(selectedRowKeys, ...currentSelectedRowKeys) - ); - - await vertexTypeStore.fetchVertexTypeList(); - - // fetch previous page data if it's empty - if ( - vertexTypeStore.requestStatus.fetchVertexTypeList === 'success' && - size(vertexTypeStore.vertexTypes) === 0 && - vertexTypeStore.vertexListPageConfig.pageNumber > 1 - ) { - vertexTypeStore.mutatePageNumber( - vertexTypeStore.vertexListPageConfig.pageNumber - 1 - ); - - vertexTypeStore.fetchVertexTypeList(); - } - - return; - } - - if (vertexTypeStore.requestStatus.deleteVertexType === 'failed') { - Message.error({ - content: vertexTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - } - }; - - const columnConfigs = [ - { - title: t('addition.vertex.vertex-type-name'), - dataIndex: 'name', - sorter: true, - sortOrder, - width: '20%', - render(text: string, records: any[], index: number) { - return ( -
    { - vertexTypeStore.selectVertexType(index); - - // check also need style infos - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - style: { - color: vertexTypeStore.selectedVertexType!.style.color, - icon: null, - size: vertexTypeStore.selectedVertexType!.style.size, - display_fields: vertexTypeStore.selectedVertexType!.style - .display_fields - } - }); - }} - > - {text} -
    - ); - } - }, - { - title: t('addition.common.association-property'), - dataIndex: 'properties', - width: '20%', - render(properties: { name: string; nullable: boolean }[]) { - const joinedProperties = - properties.length !== 0 - ? properties.map(({ name }) => name).join('; ') - : '-'; - - return ( -
    - {joinedProperties} -
    - ); - } - }, - { - title: t('addition.common.id-strategy'), - dataIndex: 'id_strategy', - width: '10%', - render(text: string) { - return ( -
    - {IDStrategyMappings[text]} -
    - ); - } - }, - { - title: t('addition.common.primary-key-property'), - dataIndex: 'primary_keys', - width: '10%', - render(properties: string[]) { - const joinedProperties = - properties.length !== 0 ? properties.join('; ') : '-'; - - return ( -
    - {joinedProperties} -
    - ); - } - }, - { - title: t('addition.menu.type-index'), - dataIndex: 'open_label_index', - width: '10%', - render(value: boolean) { - return ( - - ); - } - }, - { - title: t('addition.common.property-index'), - dataIndex: 'property_indexes', - width: '17%', - render(indexes: { name: string; type: string; fields: string[] }[]) { - const joindedIndexes = - indexes.length !== 0 - ? indexes.map(({ name }) => name).join('; ') - : '-'; - - return ( -
    - {joindedIndexes} -
    - ); - } - }, - { - title: t('addition.operate.operate'), - dataIndex: 'manipulation', - width: '13%', - render(_: any, records: VertexType, index: number) { - return ( - - ); - } - } - ]; - - const metadataDrawerOptionClass = classnames({ - 'metadata-drawer-options': true, - 'metadata-drawer-options-disabled': isEditVertex - }); - - useEffect(() => { - setTimeout(() => { - switchPreLoading(false); - }, 500); - }, [sortOrder]); - - useEffect(() => { - return () => { - const messageComponents = document.querySelectorAll( - '.new-fc-one-message' - ) as NodeListOf; - - messageComponents.forEach((messageComponent) => { - messageComponent.style.display = 'none'; - }); - }; - }); - - useEffect(() => { - if (metadataConfigsRootStore.currentId !== null) { - metadataPropertyStore.fetchMetadataPropertyList({ fetchAll: true }); - vertexTypeStore.fetchVertexTypeList(); - } - - return () => { - vertexTypeStore.dispose(); - }; - }, [ - metadataPropertyStore, - metadataConfigsRootStore.currentId, - vertexTypeStore - ]); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - if (vertexTypeStore.currentTabStatus === 'new') { - return ; - } - - if (vertexTypeStore.currentTabStatus === 'reuse') { - return ; - } - - return ( - -
    -
    - - -
    - {size(currentSelectedRowKeys) !== 0 && ( -
    -
    - {t('addition.message.selected')} - {size(currentSelectedRowKeys)} - {t('addition.common.term')} -
    - - {t('addition.common.close')} { - mutateSelectedRowKeys([]); - }} - /> -
    - )} -
    rowData.name} - locale={{ - emptyText: ( - } - /> - ) - }} - rowSelection={{ - selectedRowKeys, - onChange: handleSelectedTableRow - }} - onSortClick={handleSortClick} - dataSource={isLoading ? [] : vertexTypeStore.vertexTypes} - pagination={ - isLoading - ? null - : { - hideOnSinglePage: false, - pageNo: vertexTypeStore.vertexListPageConfig.pageNumber, - pageSize: 10, - showSizeChange: false, - showPageJumper: false, - total: vertexTypeStore.vertexListPageConfig.pageTotal, - onPageNoChange: (e: React.ChangeEvent) => { - // mutateSelectedRowKeys([]); - vertexTypeStore.mutatePageNumber(Number(e.target.value)); - vertexTypeStore.fetchVertexTypeList(); - } - } - } - /> - - { - switchShowModal(false); - }} - > -
    -
    - {t('addition.common.del-comfirm')} - {t('addition.common.close')} { - switchShowModal(false); - }} - /> -
    -
    - {t('addition.vertex.using-cannot-delete-confirm')} -
    -
    - {t('addition.message.long-time-notice')} -
    -
    ) { - return ( - - {text} - - ); - } - }, - { - title: t('addition.common.status'), - dataIndex: 'status', - render(isUsing: boolean) { - return ( -
    - {isUsing - ? t('addition.common.in-use') - : t('addition.common.not-used')} -
    - ); - } - } - ]} - dataSource={currentSelectedRowKeys.map((name) => { - return { - name, - status: - vertexTypeStore.vertexTypeUsingStatus !== null && - // data may have some delay which leads to no matching propety value - !!vertexTypeStore.vertexTypeUsingStatus[name] - }; - })} - pagination={false} - /> - - - - { - if (!isEditVertex) { - switchIsEditVertex(true); - vertexTypeStore.validateEditVertexType(); - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - style: { - color: vertexTypeStore.selectedVertexType!.style.color, - icon: null, - size: vertexTypeStore.selectedVertexType!.style.size, - display_fields: vertexTypeStore.selectedVertexType!.style - .display_fields - } - }); - } else { - await vertexTypeStore.updateVertexType(); - - if ( - vertexTypeStore.requestStatus.updateVertexType === 'failed' - ) { - Message.error({ - content: vertexTypeStore.errorMessage, - size: 'medium', - showCloseIcon: false - }); - - return; - } - - if ( - vertexTypeStore.requestStatus.updateVertexType === 'success' - ) { - if ( - isEmpty( - vertexTypeStore.editedSelectedVertexType - .append_property_indexes - ) - ) { - Message.success({ - content: t('addition.operate.modify-success'), - size: 'medium', - showCloseIcon: false - }); - } else { - Message.success({ - content: ( -
    -
    - {t('addition.common.save-scuccess')} -
    -
    - {t('addition.message.index-long-time-notice')} -
    -
    { - setLocation( - `/graph-management/${metadataConfigsRootStore.currentId}/async-tasks` - ); - }} - > - {t('addition.operate.view-task-management')} -
    -
    - ), - duration: 60 * 60 * 24 - }); - } - } - - switchIsEditVertex(false); - vertexTypeStore.selectVertexType(null); - vertexTypeStore.fetchVertexTypeList(); - vertexTypeStore.resetEditedSelectedVertexType(); - } - }} - key="drawer-manipulation" - > - {isEditVertex - ? t('addition.common.save') - : t('addition.common.edit')} - , - - ]} - > - {!isEmpty(vertexTypeStore.selectedVertexType) && ( -
    -
    - {t('addition.menu.base-info')} -
    -
    -
    - {t('addition.vertex.vertex-type-name')}: -
    -
    - {vertexTypeStore.selectedVertexType!.name} -
    -
    -
    -
    - - {t('addition.vertex.vertex-style')}: - -
    -
    - -
    -
    - -
    -
    -
    -
    - {t('addition.common.id-strategy')}: -
    - { - IDStrategyMappings[ - vertexTypeStore.selectedVertexType!.id_strategy - ] - } -
    -
    -
    - {t('addition.common.association-property')}: -
    -
    -
    - {t('addition.common.property')} - {t('addition.common.allow-null')} -
    - {vertexTypeStore.selectedVertexType!.properties.map( - ({ name, nullable }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditVertex && - vertexTypeStore.editedSelectedVertexType.append_properties.map( - ({ name }) => ( -
    -
    {name}
    -
    - -
    -
    - ) - )} - {isEditVertex && ( -
    { - switchIsAddProperty(!isAddProperty); - }} - > - - {t('addition.common.add-property')} - - toogleAddProperties -
    - )} - {isEditVertex && isAddProperty && ( -
    - {metadataPropertyStore.metadataProperties - .filter( - (property) => - vertexTypeStore.selectedVertexType!.properties.find( - ({ name }) => name === property.name - ) === undefined - ) - .map((property) => ( -
    - - - propertyIndex === property.name - ) !== -1 - } - onChange={() => { - const addedPropertiesInSelectedVertextType = - vertexTypeStore.addedPropertiesInSelectedVertextType; - - addedPropertiesInSelectedVertextType.has( - property.name - ) - ? addedPropertiesInSelectedVertextType.delete( - property.name - ) - : addedPropertiesInSelectedVertextType.add( - property.name - ); - - vertexTypeStore.mutateEditedSelectedVertexType( - { - ...vertexTypeStore.editedSelectedVertexType, - append_properties: [ - ...addedPropertiesInSelectedVertextType - ].map((propertyName) => { - const currentProperty = vertexTypeStore.newVertexType.properties.find( - ({ name }) => name === propertyName - ); - - return { - name: propertyName, - nullable: !isUndefined( - currentProperty - ) - ? currentProperty.nullable - : true - }; - }) - } - ); - }} - > - {property.name} - - -
    - ))} -
    - )} -
    -
    -
    -
    - {t('addition.common.primary-key-property')}: -
    - {vertexTypeStore.selectedVertexType!.primary_keys.join(';')} -
    -
    -
    - - {t('addition.vertex.vertex-display-content')}: - -
    - {isEditVertex ? ( - - ) : ( -
    - {vertexTypeStore.selectedVertexType?.style.display_fields - .map((field) => - formatVertexIdText( - field, - t('addition.function-parameter.vertex-id') - ) - ) - .join('-')} -
    - )} -
    - -
    - {t('addition.edge.index-info')} -
    -
    -
    - {t('addition.menu.type-index')}: -
    - -
    -
    -
    - {t('addition.common.property-index')}: -
    -
    - {(vertexTypeStore.selectedVertexType!.property_indexes - .length !== 0 || - vertexTypeStore.editedSelectedVertexType - .append_property_indexes.length !== 0) && ( -
    - {t('addition.edge.index-name')} - {t('addition.edge.index-type')} - {t('addition.common.property')} -
    - )} - {vertexTypeStore - .selectedVertexType!.property_indexes.filter( - (propertyIndex) => - isUndefined( - vertexTypeStore.editedSelectedVertexType.remove_property_indexes.find( - (removedPropertyName) => - removedPropertyName === propertyIndex.name - ) - ) - ) - .map(({ name, type, fields }, index) => { - return ( -
    -
    {name}
    -
    {propertyIndexTypeMappings[type]}
    -
    - - {fields - .map((field, index) => index + 1 + '.' + field) - .join(';')} - - - {isEditVertex && ( - -

    - {t( - 'addition.message.property-del-confirm' - )} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const removedPropertyIndexes = cloneDeep( - vertexTypeStore - .editedSelectedVertexType - .remove_property_indexes - ); - - removedPropertyIndexes.push( - vertexTypeStore.selectedVertexType! - .property_indexes[index].name - ); - - vertexTypeStore.mutateEditedSelectedVertexType( - { - ...vertexTypeStore.editedSelectedVertexType, - remove_property_indexes: removedPropertyIndexes - } - ); - - setDeleteExistPopIndexInDrawer(null); - vertexTypeStore.validateEditVertexType( - true - ); - }} - > - {t('addition.common.confirm')} -
    -
    { - setDeleteExistPopIndexInDrawer(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteExistPopIndexInDrawer(index); - } - }} - childrenWrapperElement="img" - /> - )} -
    -
    - ); - })} - {vertexTypeStore.editedSelectedVertexType.append_property_indexes.map( - ({ name, type, fields }, index) => { - return ( -
    -
    - { - const propertyIndexEntities = cloneDeep( - vertexTypeStore.editedSelectedVertexType - .append_property_indexes - ); - - propertyIndexEntities[index].name = e.value; - - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - append_property_indexes: propertyIndexEntities - }); - }} - originInputProps={{ - onBlur() { - // check is ready to create - vertexTypeStore.validateEditVertexType(); - } - }} - /> -
    -
    - -
    -
    - - - -

    - {t('addition.message.property-del-confirm')} -

    -

    - {t('addition.message.index-del-confirm')} -

    -
    -
    { - const appendPropertyIndexes = cloneDeep( - vertexTypeStore.editedSelectedVertexType! - .append_property_indexes - ); - - appendPropertyIndexes.splice(index, 1); - - vertexTypeStore.mutateEditedSelectedVertexType( - { - ...vertexTypeStore.editedSelectedVertexType, - append_property_indexes: appendPropertyIndexes - } - ); - - setDeleteAddedPopIndexInDrawer(null); - vertexTypeStore.validateEditVertexType( - true - ); - }} - > - {t('addition.common.confirm')} -
    -
    { - vertexTypeStore.resetEditedSelectedVertexType(); - setDeleteAddedPopIndexInDrawer(null); - }} - > - {t('addition.common.cancel')} -
    -
    -
    - } - childrenProps={{ - src: CloseIcon, - alt: 'close', - style: { cursor: 'pointer' }, - onClick() { - setDeleteAddedPopIndexInDrawer(index); - } - }} - childrenWrapperElement="img" - /> -
    -
    - ); - } - )} - {isEditVertex && ( - { - if ( - vertexTypeStore.editedSelectedVertexType - .append_property_indexes.length === 0 || - vertexTypeStore.isEditReady - ) { - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - append_property_indexes: [ - ...vertexTypeStore.editedSelectedVertexType - .append_property_indexes, - { - name: '', - type: '', - fields: [] - } - ] - }); - - vertexTypeStore.validateEditVertexType(true); - } - }} - style={{ - cursor: 'pointer', - color: vertexTypeStore.isEditReady ? '#2b65ff' : '#999' - }} - > - {t('addition.edge.add-group')} - - )} -
    - - - )} -
    - - - ); -}); - -export interface VertexTypeListManipulation { - vertexName: string; - vertexIndex: number; - switchIsEditVertex: (flag: boolean) => void; -} - -const VertexTypeListManipulation: React.FC = observer( - ({ vertexName, vertexIndex, switchIsEditVertex }) => { - const { vertexTypeStore } = useContext(MetadataConfigsRootStore); - const [isPopDeleteModal, switchPopDeleteModal] = useState(false); - const [isDeleting, switchDeleting] = useState(false); - const { t } = useTranslation(); - const deleteWrapperRef = useRef(null); - const isDeleteOrBatchDeleting = - isDeleting || - (vertexTypeStore.requestStatus.deleteVertexType === 'pending' && - vertexTypeStore.selectedVertexTypeNames.includes(vertexName)); - - const handleOutSideClick = useCallback( - (e: MouseEvent) => { - if ( - isPopDeleteModal && - deleteWrapperRef && - deleteWrapperRef.current && - !deleteWrapperRef.current.contains(e.target as Element) - ) { - switchPopDeleteModal(false); - } - }, - [deleteWrapperRef, isPopDeleteModal] - ); - - useEffect(() => { - document.addEventListener('click', handleOutSideClick, false); - - return () => { - document.removeEventListener('click', handleOutSideClick, false); - }; - }, [handleOutSideClick]); - - return ( -
    - { - vertexTypeStore.selectVertexType(vertexIndex); - vertexTypeStore.validateEditVertexType(true); - switchIsEditVertex(true); - - vertexTypeStore.mutateEditedSelectedVertexType({ - ...vertexTypeStore.editedSelectedVertexType, - style: { - color: vertexTypeStore.selectedVertexType!.style.color, - icon: null, - size: vertexTypeStore.selectedVertexType!.style.size, - display_fields: vertexTypeStore.selectedVertexType!.style - .display_fields - } - }); - }} - > - {t('addition.common.edit')} - -
    - {isDeleteOrBatchDeleting && ( - - )} - - {vertexTypeStore.vertexTypeUsingStatus && - vertexTypeStore.vertexTypeUsingStatus[vertexName] ? ( -

    - {t('addition.vertex.using-cannot-delete')} -

    - ) : ( - <> -

    - {t('addition.vertex.del-vertex-confirm')} -

    -

    {t('addition.vertex.del-vertex-confirm-again')}

    -

    {t('addition.message.long-time-notice')}

    -
    - - -
    - - )} -
    - } - childrenProps={{ - className: 'metadata-properties-manipulation', - title: isDeleteOrBatchDeleting - ? t('addition.operate.del-ing') - : t('addition.common.del'), - async onClick() { - if (isDeleteOrBatchDeleting) { - return; - } - - await vertexTypeStore.checkIfUsing([vertexName]); - - if (vertexTypeStore.requestStatus.checkIfUsing === 'success') { - switchPopDeleteModal(true); - } - } - }} - > - {isDeleteOrBatchDeleting - ? t('addition.operate.del-ing') - : t('addition.common.del')} - -
    - - ); - } -); - -const EmptyVertxTypeHints: React.FC = observer(() => { - const { vertexTypeStore } = useContext(MetadataConfigsRootStore); - const { t } = useTranslation(); - - return ( -
    - Add new property -
    - {t('addition.vertex.no-vertex-type-desc')} -
    -
    - - -
    -
    - ); -}); - -export default VertexTypeList; diff --git a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/index.ts b/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/index.ts deleted file mode 100644 index acd50f8f0..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/graph-management/metadata-configs/vertex-type/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import VertexTypeList from './VertexTypeList'; -import NewVertexType from './NewVertexType'; - -export { NewVertexType, VertexTypeList }; diff --git a/hugegraph-hubble/hubble-fe/src/components/hubble-ui/index.tsx b/hugegraph-hubble/hubble-fe/src/components/hubble-ui/index.tsx deleted file mode 100644 index f856514e6..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/hubble-ui/index.tsx +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -/* eslint-disable */ -import { - Transfer as TransferAntD, - Popover, - Pagination as PaginationAntD, - Steps as StepsAntD, - Progress as ProgressAntD, - Checkbox as CheckboxAntD, - Menu as MenuAntD, - Spin, - Breadcrumb as BreadcrumbAntD, - Calendar as CalendarAntD, - InputNumber, - Switch as SwitchAntd, - Table as TableAntD, - Radio as RadioAntD, - Tooltip as TooltipAntD, - Alert as AlertAntD, - Button as ButtonAntD, - Modal as ModalAntD, - Drawer as DrawerAntD, - Input as InputAntD, - message, - Select as SelectAntD, - Dropdown as DropdownAntd -} from 'antd'; -import * as React from 'react'; - -// In order to make the project run, let the components of antd replace baiduUI -// Special Note: All check boxes can be selected, but some check boxes cannot see -// the selection content, but it does not affect the submission. If you need to see -// the selection, you need to modify the value property of the corresponding Select -// component. There is no uniform modification here - -const changeSize = (props: any): any => { - let _size = props.size; - if (_size === 'medium') { - _size = 'middle'; - } - if (!_size) { - _size = 'small'; - } - return { - ...props, - size: _size - }; -}; - -export const Alert = (props: any) => { - return ; -}; - -export const Button = (props: any) => { - return {props.children}; -}; - -export const Modal = ModalAntD; - -export const Drawer = (props: any) => { - return {props.children}; -}; - -export const Input = (props: any) => { - let _blur = () => {}; - if (props.originInputProps && props.originInputProps.onBlur) { - _blur = props.originInputProps.onBlur; - } - // change e.value to eventTarget.currentTarget - const _props = { - ...props, - onChange: (e: any) => { - props.onChange({ - value: e.currentTarget.value - }); - } - }; - return ( -
    - - {props.errorMessage ? ( -
    - {props.errorMessage} -
    - ) : ( - '' - )} -
    - ); -}; -Input.Search = (props: any) => { - return ( - - ); -}; - -export const Message = { - info: (data: any) => { - message.info(data.content); - }, - success: (data: any) => { - message.success(data.content); - }, - error: (data: any) => { - message.error(data.content); - }, - warning: (data: any) => { - message.warning(data.content); - }, - loading: (data: any) => { - message.loading(data.content); - } -}; - -export const Select: any = (props: any) => { - return ( - - {props.children} - - ); -}; -Select.Option = SelectAntD.Option; -export const Tooltip: any = TooltipAntD; - -export const Dropdown: any = { - Button(props: any) { - let _overlay: any = []; - if (props.options) { - _overlay = ( - - {props.options.map((item: any) => ( - - {item.label} - - ))} - - ); - } - - return ( - {}) - }} - > - {props.title} - - ); - } -}; -export const Radio: any = RadioAntD; - -export const Table: any = (props: any) => { - let pagination = {}; - let pageChangerTag = false; - if (props.pagination) { - pagination = { - ...props.pagination, - onChange: (page: any, size: any) => { - if (pageChangerTag) { - return; - } - props.pagination.onPageNoChange({ - target: { - value: page - } - }); - }, - showQuickJumper: props.pagination.showPageJumper, - current: props.pagination.pageNo, - onShowSizeChange: (e: any, size: any) => { - pageChangerTag = true; - props.pagination.onPageSizeChange({ - target: { - value: size - } - }); - setTimeout(() => { - pageChangerTag = false; - }); - } - }; - } - let _handleChange: any = props.onChange || (() => {}); - // able to sort - if (!props.onChange && props.onSortClick) { - _handleChange = props.onSortClick; - } - return ( - - {props.children} - - ); -}; - -export const Switch: any = (props: any) => { - return ( - - ); -}; - -export const NumberBox: any = InputNumber; - -export const Calendar = (props: any) => { - props.onSelect = props.onSelectDay; - return ; -}; -export const Breadcrumb: any = BreadcrumbAntD; - -export const Loading: any = Spin; - -export const Menu: any = MenuAntD; - -export const Checkbox: any = CheckboxAntD; - -export const Progress: any = ProgressAntD; - -export const Steps: any = StepsAntD; - -export const Embedded: any = (props: any) => { - return ( - - {props.children} - - ); -}; -export const Pagination = (props: any) => { - return ( - - ); -}; - -export const PopLayer: any = (props: any) => { - return ( - {props.children} - ); -}; - -export const Transfer: any = (props: any) => { - const _treeName = props.treeName; - const dataSource = props.dataSource || []; - const allDataMap = props.allDataMap; - if (allDataMap) { - for (const key in allDataMap) { - dataSource.push(allDataMap[key]); - } - } - return ( - { - props.handleSelect(targetKeys); - }, - titles: [`可选${_treeName}`, `已选${_treeName}`], - render: (item) => `${item.title}` - }} - > - {props.children} - - ); -}; diff --git a/hugegraph-hubble/hubble-fe/src/components/hubble-ui/src/index.less b/hugegraph-hubble/hubble-fe/src/components/hubble-ui/src/index.less deleted file mode 100644 index a7e8f030e..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/hubble-ui/src/index.less +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -@import '~antd/dist/antd.css'; -.new-fc-one-input-count-visible { - display: inline-block; -} - -.new-fc-one-input-count-error { - color: #e64552; -} - -.new-fc-one-input-all-container, -.new-fc-one-input-all-container .new-fc-one-input-detail { - display: inline-block; - position: relative; -} - -.new-fc-one-input-all-container-error .new-fc-one-input { - border: 1px solid #e64552; - background-color: #fff; - color: #333; -} - -.new-fc-one-input-all-container-error .new-fc-one-input:active, -.new-fc-one-input-all-container-error .new-fc-one-input:focus, -.new-fc-one-input-all-container-error .new-fc-one-input:hover { - outline: 0; - border-color: #e64552; -} - -.new-fc-one-input-all-container-error .new-fc-one-input:focus { - box-shadow: 0 0 0 2px rgba(230, 69, 82, 0.2); -} - -.new-fc-one-input-error { - color: #e64552; -} - -.new-fc-one-input-error-bottom { - margin-top: 8px; -} - -.new-fc-one-input-error-right { - display: inline-block; - margin-left: 8px; -} - -.new-fc-one-input-error-layer { - line-height: 100%; - z-index: 2; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15); - background: #fff; - position: absolute; - left: 0; - display: inline-block; - padding: 16px; - color: #e64552; -} - -.new-fc-one-input-error-layer:before { - content: ''; - width: 0; - height: 0; - left: 16px; - border: 8px solid transparent; - border-bottom-color: #fff; - top: -12px; - position: absolute; -} - -.new-fc-one-input-group-addon > div { - border: 1px solid #e0e0e0 !important; -} - -.new-fc-one-input-group-addon-before > div { - border-right: 0 !important; -} - -.new-fc-one-input-group-addon-after > div { - border-left: 0 !important; -} - -.new-fc-one-input-group { - display: flex; -} - -.new-fc-one-input-group .new-fc-one-input { - border-radius: 0; -} diff --git a/hugegraph-hubble/hubble-fe/src/customHook/useCustomEdge.js b/hugegraph-hubble/hubble-fe/src/customHook/useCustomEdge.js new file mode 100644 index 000000000..a0ba486e6 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/customHook/useCustomEdge.js @@ -0,0 +1,67 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 自定义边 + */ + +import G6 from '@antv/g6'; +import {useEffect} from 'react'; + +const options = { + afterDraw(cfg, group) { + const shape = group.get('children')[0]; + const startPoint = shape.getPoint(0); + const circle = group.addShape('circle', { + attrs: { + x: startPoint.x, + y: startPoint.y, + fill: cfg.style.stroke, + r: 3, + }, + name: 'circle-shape', + }); + circle.animate( + ratio => { + const tmpPoint = shape.getPoint(ratio); + return { + x: tmpPoint.x, + y: tmpPoint.y, + }; + }, + { + repeat: true, // Whether executes the animation repeatly + duration: 3000, // the duration for executing once + } + ); + }, + update: undefined, +}; + +const useCustomEdge = () => { + useEffect( + () => { + G6.registerEdge('runningLine', options, 'line'); + G6.registerEdge('runningQuadratic', options, 'quadratic'); + G6.registerEdge('runningLoop', options, 'loop'); + }, + [] + ); +}; + +export default useCustomEdge; diff --git a/hugegraph-hubble/hubble-fe/src/customHook/useCustomGrid.js b/hugegraph-hubble/hubble-fe/src/customHook/useCustomGrid.js new file mode 100644 index 000000000..99a0d2ab4 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/customHook/useCustomGrid.js @@ -0,0 +1,306 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 自定义网格布局 + */ + +import _ from 'lodash'; +import G6 from '@antv/g6'; +import {useEffect} from 'react'; + +const CUSTOM_GRID_REGISTERED = '__hubbleCustomGridRegistered'; + +const options = { + small(val) { + const self = this; + let res; + const rows = self.rows; + const cols = self.cols; + if (val == null) { + res = Math.min(rows, cols); + } + else { + const min = Math.min(rows, cols); + if (min === self.rows) { + self.rows = val; + } + else { + self.cols = val; + } + } + return res; + }, + + large(val) { + const self = this; + let res; + const rows = self.rows; + const cols = self.cols; + if (val == null) { + res = Math.max(rows, cols); + } + else { + const max = Math.max(rows, cols); + if (max === self.rows) { + self.rows = val; + } + else { + self.cols = val; + } + } + return res; + }, + + used(row, col) { + const self = this; + return self.cellUsed[`c-${row}-${col}`] || false; + }, + + use(row, col) { + const self = this; + self.cellUsed[`c-${row}-${col}`] = true; + }, + + // 依次排完一行后,再往下排 ,固定列数 + moveToNextCell() { + const self = this; + + const cols = self.cols || 5; + self.col++; + if (self.col >= cols) { + self.col = 0; + self.row++; + } + + }, + + // 依次排完一列后,再往右排,固定行数 + moveToNextRow() { + const self = this; + const rows = self.rows || 5; + self.row++; + if (self.row >= rows) { + self.row = 0; + self.col++; + } + }, + + getPos(node) { + const self = this; + const begin = [0, 0]; + const cellWidth = self.cellWidth; + const cellHeight = self.cellHeight; + let x; + let y; + + const rcPos = self.id2manPos[node.id]; + if (rcPos) { + x = rcPos.col * cellWidth + cellWidth / 2 + begin[0]; + y = rcPos.row * cellHeight + cellHeight / 2 + begin[1]; + } + else { + + while (self.used(self.row, self.col)) { + if (self.cols > self.rows) { + self.moveToNextCell(); + } + else { + self.moveToNextRow(); + } + } + + x = self.col * cellWidth + cellWidth / 2 + begin[0]; + y = self.row * cellHeight + cellHeight / 2 + begin[1]; + self.use(self.row, self.col); + if (self.cols > self.rows) { + self.moveToNextCell(); + } + else { + self.moveToNextRow(); + } + } + node.x = x; + node.y = y; + }, + + /** + * 执行布局 + */ + execute() { + const self = this; + + const nodes = self.nodes; + const n = nodes.length; + const center = self.center; + const preventOverlap = true; + if (n === 0) { + return; + } + if (n === 1) { + nodes[0].x = center[0]; + nodes[0].y = center[1]; + return; + } + + const layoutNodes = []; + nodes.forEach(node => { + layoutNodes.push(node); + }); + const nodeIdxMap = {}; + layoutNodes.forEach((node, i) => { + nodeIdxMap[node.id] = i; + }); + // .......其他排序 + // 排序 + layoutNodes.sort((n1, n2) => n2 - n1); + + if (!self.width && typeof window !== 'undefined') { + self.width = window.innerWidth; + } + if (!self.height && typeof window !== 'undefined') { + self.height = window.innerHeight; + } + + const oRows = self.rows; + const oCols = self.cols != null ? self.cols : self.columns; + self.cells = n; + + // if rows or columns were set in self, use those values + if (oRows != null && oCols != null) { + self.rows = oRows; + self.cols = oCols; + } + else if (oRows != null && oCols == null) { + self.rows = oRows; + self.cols = Math.ceil(self.cells / self.rows); + } + else if (oRows == null && oCols != null) { + self.cols = oCols; + self.rows = Math.ceil(self.cells / self.cols); + } + else { + self.splits = Math.sqrt((self.cells * self.height) / self.width); + self.rows = Math.round(self.splits); + self.cols = Math.round((self.width / self.height) * self.splits); + } + + self.cellWidth = self.width / self.cols; + self.cellHeight = self.height / self.rows; + + if (self.condense) { + self.cellWidth = 0; + self.cellHeight = 0; + } + + if (preventOverlap) { + layoutNodes.forEach(node => { + if (!node.x || !node.y) { + node.x = 0; + node.y = 0; + } + + let nodew; + let nodeh; + if (_.isArray(node.size)) { + nodew = node.size[0]; + nodeh = node.size[1]; + } + else if (_.isNumber(node.size)) { + nodew = node.size; + nodeh = node.size; + } + if (nodew === undefined || nodeh === undefined) { + if (_.isArray(self.nodeSize)) { + nodew = self.nodeSize[0]; + nodeh = self.nodeSize[1]; + } + else if (_.isNumber(self.nodeSize)) { + nodew = self.nodeSize; + nodeh = self.nodeSize; + } + else { + nodew = 30; + nodeh = 30; + } + } + + const p = 30; + + const w = nodew + p; + const h = nodeh + p; + + self.cellWidth = Math.max(self.cellWidth, w); + self.cellHeight = Math.max(self.cellHeight, h); + }); + } + + self.cellUsed = {}; // e.g. 'c-0-2' => true + + self.row = 0; + self.col = 0; + + self.id2manPos = {}; + for (let i = 0; i < layoutNodes.length; i++) { + const node = layoutNodes[i]; + let rcPos; + if (self.position) { + rcPos = self.position(node); + } + + if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) { + const pos = { + row: rcPos.row, + col: rcPos.col, + }; + + if (pos.col === undefined) { + pos.col = 0; + while (self.used(pos.row, pos.col)) { + pos.col++; + } + } + else if (pos.row === undefined) { + pos.row = 0; + + while (self.used(pos.row, pos.col)) { + pos.row++; + } + } + + self.id2manPos[node.id] = pos; + self.use(pos.row, pos.col); + } + self.getPos(node); + } + }, +}; + +const useCustomGrid = () => { + useEffect( + () => { + if (!G6[CUSTOM_GRID_REGISTERED]) { + G6.registerLayout('customGrid', options); + G6[CUSTOM_GRID_REGISTERED] = true; + } + }, + [] + ); +}; + +export default useCustomGrid; diff --git a/hugegraph-hubble/hubble-fe/src/customHook/useCustomNode.js b/hugegraph-hubble/hubble-fe/src/customHook/useCustomNode.js new file mode 100644 index 000000000..7aff2844a --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/customHook/useCustomNode.js @@ -0,0 +1,176 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 自定义节点 + */ + +import icons from '../utils/graph'; +import G6 from '@antv/g6'; +import {useEffect} from 'react'; + +const getBadgePosition = (size, type) => { + let badgeX = 0; + let badgeY = 0; + if (type === 'circle') { + const r = size / 2; + badgeX = r * Math.cos((Math.PI * 7) / 4); + badgeY = -r * Math.sin((Math.PI * 7) / 4); + } + else if (type === 'diamond') { + const r = size / 2; + badgeX = r / 2; + badgeY = r / 2; + } + else if (type === 'triangle') { + const r = size; + badgeX = r * Math.cos((Math.PI) / 6); + badgeY = r - 5; + } + else if (type === 'star') { + const r = size / 2; + badgeX = r; + badgeY = 1.39 * r; + } + else if (type === 'ellipse') { + badgeX = size[0] / 4; + badgeY = size[1] / 3; + } + return { + x: badgeX, + y: badgeY, + }; +}; + +const drawBadge = (group, size, type) => { + const [width, height] = [10, 10]; + const {x: badgeX, y: badgeY} = getBadgePosition(size, type); + if (width === height) { + const shape = { + attrs: { + r: 5, + fill: 'grey', + x: badgeX, + y: badgeY, + }, + name: 'badges-circle', + id: 'badges-circle', + }; + group.addShape('circle', shape); + } + group.addShape('text', { + attrs: { + x: badgeX, + y: badgeY, + fontFamily: 'graphin', + text: icons.pushpin, + textAlign: 'center', + textBaseline: 'middle', + fontSize: 8, + color: '#fff', + fill: '#fff', + }, + capture: false, + name: 'badges-text', + id: 'badges-text', + }); +}; + +const removeBadge = group => { + const a = group.findById('badges-circle'); + group.removeChild(a); + const b = group.findById('badges-text'); + group.removeChild(b); +}; + +const options = { + setState(name, value, item) { + if (!name) { + return; + } + const group = item.getContainer(); + const groupChildren = group?.get('children'); + if (groupChildren) { + const shape = group?.get('children')[0]; + const model = item.getModel(); + const {stateStyles = {}, size, type} = model; + const currentStateStyle = stateStyles[name] || ''; + const status = item._cfg?.states || []; + if (value) { + Object.entries(currentStateStyle).forEach( + item => { + shape.attr(item[0], item[1]); + } + ); + // 固定节点增加样式 + if (name === 'customFixed') { + // 如果有icon就不添加 + const badgeGroup = group.findById('badges-circle'); + if (badgeGroup == null) { + drawBadge(group, size, type); + } + } + } + else { + if (name === 'customFixed') { + removeBadge(group); + } + Object.entries(currentStateStyle).forEach( + item => { + const [key] = item; + shape.attr(key, model.style[key]); + } + ); + // 如果有其他状态 设置过去; + if (status.length > 0) { + status.forEach(key => { + const currentStateStyle = stateStyles[key] || {}; + Object.entries(currentStateStyle).forEach( + item => { + shape.attr(item[0], item[1]); + } + ); + if (name === 'customFixed') { + // 如果有icon就不添加 + const badgeGroup = group.findById('badges-circle'); + if (badgeGroup == null) { + drawBadge(group, size, type); + } + } + } + ); + } + } + } + }, +}; + +const useCustomNode = () => { + useEffect( + () => { + G6.registerNode('circle', options, 'circle'); + G6.registerNode('diamond', options, 'diamond'); + G6.registerNode('triangle', options, 'triangle'); + G6.registerNode('star', options, 'star'); + G6.registerNode('ellipse', options, 'ellipse'); + }, + [] + ); +}; + +export default useCustomNode; diff --git a/hugegraph-hubble/hubble-fe/src/customHook/useDownloadJson.js b/hugegraph-hubble/hubble-fe/src/customHook/useDownloadJson.js new file mode 100644 index 000000000..679f1fb5c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/customHook/useDownloadJson.js @@ -0,0 +1,56 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 下载Json数据 + */ + +const serializeDownloadJson = data => JSON.stringify( + data, + (key, value) => (typeof value === 'bigint' ? value.toString() : value) +); + +const useDownloadJson = () => { + + const downloadJsonHandler = (fileName, data) => { + const sanitizedFileName = String(fileName || '') + .trim() + .replace(/\.json$/i, '') + .split('.') + .join(''); + const formatedFileName = `${sanitizedFileName || 'graph-data'}.json`; + const element = document.createElement('a'); + const processedData = serializeDownloadJson(data); + const blob = new Blob([processedData], { + type: 'application/json;charset=utf-8', + }); + const objectUrl = URL.createObjectURL(blob); + element.setAttribute('href', objectUrl); + element.setAttribute('download', formatedFileName); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + URL.revokeObjectURL(objectUrl); + }; + + return {downloadJsonHandler}; +}; + +export {serializeDownloadJson}; +export default useDownloadJson; diff --git a/hugegraph-hubble/hubble-fe/src/customHook/useDownloadJson.test.js b/hugegraph-hubble/hubble-fe/src/customHook/useDownloadJson.test.js new file mode 100644 index 000000000..32bbb49e7 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/customHook/useDownloadJson.test.js @@ -0,0 +1,81 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import useDownloadJson, {serializeDownloadJson} from './useDownloadJson'; +import JSONbig from 'json-bigint'; + +const readBlob = blob => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsText(blob); +}); + +describe('useDownloadJson', () => { + let createObjectURL; + let revokeObjectURL; + let anchor; + + beforeEach(() => { + createObjectURL = jest.fn(() => 'blob:export-json'); + revokeObjectURL = jest.fn(); + global.URL.createObjectURL = createObjectURL; + global.URL.revokeObjectURL = revokeObjectURL; + + anchor = document.createElement('a'); + jest.spyOn(anchor, 'click').mockImplementation(() => {}); + jest.spyOn(document, 'createElement').mockReturnValue(anchor); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('downloads graph data as a json file through a blob url', () => { + const {downloadJsonHandler} = useDownloadJson(); + + downloadJsonHandler('codexjson', {vertices: [], edges: []}); + + expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob)); + expect(anchor.getAttribute('download')).toBe('codexjson.json'); + expect(anchor.getAttribute('href')).toBe('blob:export-json'); + expect(anchor.click).toHaveBeenCalledTimes(1); + expect(revokeObjectURL).toHaveBeenCalledWith('blob:export-json'); + }); + + it('serializes native and parsed large ids without precision loss', () => { + const parsedId = JSONbig.parse('{"id":9007199254740993}').id; + + expect(serializeDownloadJson({native: 9007199254740993n, parsedId})) + .toBe('{"native":"9007199254740993","parsedId":"9007199254740993"}'); + }); + + it('downloads standard JSON that round-trips large ids from the actual blob', async () => { + const parsedId = JSONbig.parse('{"id":9007199254740993}').id; + const {downloadJsonHandler} = useDownloadJson(); + + downloadJsonHandler('large-ids', {native: 9007199254740993n, parsedId}); + + const blob = createObjectURL.mock.calls[0][0]; + const downloaded = JSON.parse(await readBlob(blob)); + expect(downloaded).toEqual({ + native: '9007199254740993', + parsedId: '9007199254740993', + }); + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/dropdown-menu-api.test.js b/hugegraph-hubble/hubble-fe/src/dropdown-menu-api.test.js new file mode 100644 index 000000000..f16e86cfd --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/dropdown-menu-api.test.js @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import fs from 'fs'; +import path from 'path'; + +const dropdownSources = [ + 'pages/Role/index.js', + 'pages/Graph/Card.js', + 'pages/GraphSpace/Card.js', + 'modules/analysis/QueryBar/ContentCommon/index.js', + 'modules/component/NewConfig/index.js', + 'modules/component/ExportData/index.js', +]; + +test('reachable Dropdowns use the Ant Design menu API', () => { + const offenders = dropdownSources.filter(file => { + const source = fs.readFileSync(path.join(__dirname, file), 'utf8'); + return / { - const init = async (params: Record) => { - const { id, jobId, status } = params!; - // sidebar data - graphManagementStore.fetchIdList(); - - // init importManagerStore - importManagerStore.setCurrentId(Number(id)); - // import job list - await importManagerStore.fetchImportJobList(); - importManagerStore.setSelectedJob(Number(jobId)); - - // init dataImportRootStore - dataImportRootStore.setCurrentId(Number(id)); - dataImportRootStore.setCurrentJobId(Number(jobId)); - - await dataMapStore.fetchDataMaps(); - - const job = importManagerStore.importJobList.find( - ({ id: _jobId }) => _jobId === Number(jobId) - ); - - if (isUndefined(job)) { - return; - } - - const defautlSelectedFileId = dataMapStore.fileMapInfos.filter( - ({ file_status }) => file_status === 'COMPLETED' - )[0]?.id; - - dataImportRootStore.setCurrentStatus(job.job_status); - - if (status === 'upload') { - if (job.job_status === 'DEFAULT' || job.job_status === 'UPLOADING') { - } - - if (job.job_status === 'MAPPING') { - dataMapStore.setSelectedFileId(defautlSelectedFileId); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - - // setLocation( - // `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/mapping` - // ); - } - - if (job.job_status === 'SETTING' || job.job_status === 'LOADING') { - dataMapStore.setSelectedFileId(defautlSelectedFileId); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - serverDataImportStore.switchIrregularProcess(true); - - // setLocation( - // `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/loading` - // ); - } - - if (job.job_status === 'LOADING') { - dataMapStore.switchLock(true); - serverDataImportStore.switchImportConfigReadOnly(true); - - setLocation( - `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/loading` - ); - } - - if (job.job_status === 'SUCCESS' || job.job_status === 'FAILED') { - setLocation(`/graph-management/${id}/data-import/import-manager`); - importManagerStore.setSelectedJob(null); - } - } - - if (status === 'mapping') { - if (job.job_status === 'DEFAULT' || job.job_status === 'UPLOADING') { - dataMapStore.setSelectedFileId(defautlSelectedFileId); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - - // setLocation( - // `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/mapping` - // ); - } - - if (job.job_status === 'MAPPING') { - dataMapStore.setSelectedFileId(defautlSelectedFileId); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - } - - if (job.job_status === 'SETTING' || job.job_status === 'LOADING') { - dataMapStore.setSelectedFileId(defautlSelectedFileId); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - serverDataImportStore.switchIrregularProcess(true); - - // setLocation( - // `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/loading` - // ); - } - - if (job.job_status === 'LOADING') { - dataMapStore.switchLock(true); - serverDataImportStore.switchImportConfigReadOnly(true); - setLocation( - `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/loading` - ); - } - - if (job.job_status === 'SUCCESS' || job.job_status === 'FAILED') { - setLocation(`/graph-management/${id}/data-import/import-manager`); - importManagerStore.setSelectedJob(null); - } - } - - if (status === 'loading') { - if (job.job_status === 'DEFAULT' || job.job_status === 'UPLOADING') { - setLocation( - `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/upload` - ); - } - - if (job.job_status === 'MAPPING') { - dataMapStore.setSelectedFileId(defautlSelectedFileId); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - - setLocation( - `/graph-management/${id}/data-import/import-manager/${jobId}/import-tasks/mapping` - ); - } - - if (job.job_status === 'SETTING' || job.job_status === 'LOADING') { - dataMapStore.setSelectedFileId(defautlSelectedFileId); - dataMapStore.setSelectedFileInfo(); - dataMapStore.switchIrregularProcess(true); - - serverDataImportStore.syncImportConfigs( - dataMapStore.selectedFileInfo!.load_parameter - ); - serverDataImportStore.switchIrregularProcess(true); - } - - if (job.job_status === 'SETTING') { - dataMapStore.switchReadOnly(false); - } - - if (job.job_status === 'LOADING') { - dataMapStore.switchLock(true); - serverDataImportStore.switchImportConfigReadOnly(true); - } - - if (job.job_status === 'SUCCESS' || job.job_status === 'FAILED') { - setLocation(`/graph-management/${id}/data-import/import-manager`); - importManagerStore.setSelectedJob(null); - } - } - - setInitReady(true); - }; - - // if importJobList is empty, users may refresh their page - // if fileMapInfos is empty, users may click back/forward button on browser - if ( - !isNull(params) && - (isEmpty(importManagerStore.importJobList) || - isNull(importManagerStore.selectedJob)) - ) { - init(params); - } - }, [ - params?.status, - importManagerStore.importJobList, - importManagerStore.selectedJob?.id - ]); - - return initReady; -} diff --git a/hugegraph-hubble/hubble-fe/src/hooks/useLocationWithConfirmation.tsx b/hugegraph-hubble/hubble-fe/src/hooks/useLocationWithConfirmation.tsx deleted file mode 100644 index 27eff539d..000000000 --- a/hugegraph-hubble/hubble-fe/src/hooks/useLocationWithConfirmation.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import { useContext } from 'react'; -import useLocation from 'wouter/use-location'; -import { last } from 'lodash-es'; - -import i18next from '../i18n'; -import { - ImportManagerStoreContext, - DataImportRootStoreContext -} from '../stores'; - -export default function useLocationWithConfirmation() { - const importManagerStore = useContext(ImportManagerStoreContext); - const dataImportRootStore = useContext(DataImportRootStoreContext); - const [location, setLocation] = useLocation(); - const status = last(location.split('/')); - - return [ - location, - (newLocation: string) => { - let perfomNavigation = true; - const category = last(newLocation.split('/')); - - if ( - status === 'upload' && - category === 'import-manager' && - dataImportRootStore.fileUploadTasks.some( - ({ status }) => status === 'uploading' - ) - ) { - perfomNavigation = window.confirm( - i18next.t('server-data-import.hint.confirm-navigation') - ); - } - - if (perfomNavigation) { - if ( - ['upload', 'mapping', 'loading'].includes(String(status)) && - category === 'import-manager' - ) { - importManagerStore.setSelectedJob(null); - importManagerStore.fetchImportJobList(); - } - - setLocation(newLocation); - } - } - ]; -} diff --git a/hugegraph-hubble/hubble-fe/src/hooks/useMultiKeyPress.tsx b/hugegraph-hubble/hubble-fe/src/hooks/useMultiKeyPress.tsx deleted file mode 100644 index c206df56a..000000000 --- a/hugegraph-hubble/hubble-fe/src/hooks/useMultiKeyPress.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import { useState, useEffect } from 'react'; - -export default function useMultiKeyPress() { - const [keysPressed, setKeyPressed] = useState>(new Set()); - - const keydownHandler = (e: KeyboardEvent) => { - // use e.key here may cause some unexpected behavior with shift key - setKeyPressed((prev) => new Set(prev.add(e.code))); - }; - - const keyupHandler = (e: KeyboardEvent) => { - // use e.key here may cause some unexpected behavior with shift key - if (navigator.platform.includes('Mac') && e.code.includes('Meta')) { - // weired, like above we need to mutate keysPressed first - keysPressed.clear(); - setKeyPressed(new Set()); - return; - } - - keysPressed.delete(e.code); - setKeyPressed(new Set(keysPressed)); - }; - - useEffect(() => { - window.addEventListener('keydown', keydownHandler); - window.addEventListener('keyup', keyupHandler); - - return () => { - window.removeEventListener('keydown', keydownHandler); - window.removeEventListener('keyup', keyupHandler); - }; - }, []); - - return keysPressed; -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/index.js b/hugegraph-hubble/hubble-fe/src/i18n/index.js new file mode 100644 index 000000000..ae40a915e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/index.js @@ -0,0 +1,38 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import i18n from 'i18next'; +import {initReactI18next} from 'react-i18next'; + +import {zhCNResources, enUSResources} from './resources'; + +i18n.use(initReactI18next).init({ + lng: localStorage.getItem('languageType') || 'zh-CN', + fallbackLng: 'zh-CN', + + resources: { + 'zh-CN': zhCNResources, + 'en-US': enUSResources, + }, + + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, +}); + +export default i18n; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/index.ts b/hugegraph-hubble/hubble-fe/src/i18n/index.ts deleted file mode 100644 index 4ecdfc2d1..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -import { zhCNResources, enUSResources } from './resources'; - -i18n.use(initReactI18next).init({ - lng: localStorage.getItem('languageType') || 'zh-CN', - fallbackLng: 'zh-CN', - - resources: { - 'zh-CN': zhCNResources, - 'en-US': enUSResources - }, - - interpolation: { - escapeValue: false // not needed for react as it escapes by default - } -}); - -export default i18n; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/ERView.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/ERView.json new file mode 100644 index 000000000..897821193 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/ERView.json @@ -0,0 +1,30 @@ +{ + "ERView": { + "edge": { + "name": "Edge", + "type": "Edge Type", + "start": "Source ID", + "end": "Target ID", + "create": "Add Edge", + "e1name": "Edge 1", + "e2name": "Edge 2", + "out": "Outgoing", + "in": "Incoming", + "both": "Both" + }, + "vertex": { + "name": "Vertex", + "type": "Vertex Type", + "create": "Add Vertex", + "v1name": "Vertex 1", + "v2name": "Vertex 2" + }, + "control": { + "zoom_in": "Zoom In", + "zoom_out": "Zoom Out", + "undo": "Undo", + "redo": "Redo", + "auto_map": "Auto Map" + } + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/board.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/board.json new file mode 100644 index 000000000..a9b7d5062 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/board.json @@ -0,0 +1,15 @@ +{ + "Topbar": { + "exit":{ + "name": "Log Out", + "confirm":"Confirm log out?", + "success":"Logged out successfully" + } + }, + "selector": { + "placeholder": "Please select" + }, + "navigation": { + "name": "Navigation" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/common.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/common.json new file mode 100644 index 000000000..44f8f3a96 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/common.json @@ -0,0 +1,37 @@ +{ + "common" : { + "verify" : { + "ok": "OK", + "cancel": "Cancel", + "yes": "Yes", + "no": "No" + }, + "status": { + "new": "New", + "running": "Running", + "success": "Completed", + "cancelling": "Stopping", + "cancelled": "Stopped", + "failed": "Failed", + "undefined": "Unknown" + }, + "validation": { + "required": "This field is required", + "max": "Maximum length is {{max}} characters", + "invalid_ip": "Invalid IP address", + "invalid_port": "Invalid port", + "invalid_cron": "Invalid Quartz expression", + "name_rule": "Start with a letter, and use lowercase letters, numbers, or underscores only", + "cn_name_rule": "Use Chinese characters, letters, or underscores only", + "property_name_rule": "Use Chinese characters, letters, numbers, or underscores only", + "normal_name_rule": "Use Chinese characters, letters, numbers, or underscores only, up to 20 characters", + "jdbc_rule": "Enter a valid JDBC URL, for example: jdbc:mysql://127.0.0.1:3306/db_name", + "account_name_rule": "Account name must be within 16 characters and cannot start or end with an underscore", + "invalid_data_format": "Invalid data format" + } + }, + "request": { + "failed": "Request failed", + "error": "Request failed: {{message}}, path: {{path}}" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/index.js b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/index.js new file mode 100644 index 000000000..30ff1ae6f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/index.js @@ -0,0 +1,26 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import Common from './common.json'; +import Board from './board.json'; +import ERView from './ERView.json'; +export { + Common, + Board, + ERView, +}; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/AsyncTasks.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/AsyncTasks.json deleted file mode 100644 index d02ae887f..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/AsyncTasks.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "async-tasks": { - "title": "Task Management", - "placeholders": { - "search": "Enter task ID or name to search" - }, - "table-column-title": { - "task-id": "Task ID", - "task-name": "Task Name", - "task-type": "Task Type", - "create-time": "Create Time", - "time-consuming": "Time Consuming", - "status": "Status", - "manipulation": "Operation" - }, - "table-filters": { - "task-type": { - "all": "All", - "gremlin": "Gremlin Task", - "algorithm": "Algorithm Task", - "remove-schema": "Remove Metadata", - "create-index": "Create Index", - "rebuild-index": "Rebuild Index" - }, - "status": { - "all": "All", - "scheduling": "Scheduling", - "scheduled": "Queued", - "queued": "Queued", - "running": "Running", - "restoring": "Restoring", - "success": "Success", - "failed": "Failed", - "cancelling": "Cancelled", - "cancelled": "Cancelled" - } - }, - "table-selection": { - "selected": "Selected {{number}} items", - "delete-batch": "Batch Delete" - }, - "manipulations": { - "abort": "Abort", - "aborting": "Aborting", - "delete": "Delete", - "check-result": "Check Result", - "check-reason": "Check Reason" - }, - "hint": { - "delete-confirm": "Delete Confirmation", - "delete-description": "Are you sure you want to delete this task? Deletion is irreversible, please proceed with caution", - "delete-succeed": "Delete Successful", - "delete-batch-confirm": "Batch Delete", - "delete-batch-description": "Confirm deletion of the following tasks? Deletion is irreversible, please proceed with caution", - "delete": "Delete", - "cancel": "Cancel", - "no-data": "No Data Available", - "empty": "You currently have no tasks", - "select-disabled": "Task {{id}} cannot be selected for deletion", - "check-details": "Check Details", - "creation-failed": "Creation Failed" - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/GraphManagementSidebar.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/GraphManagementSidebar.json deleted file mode 100644 index 4ffabfc8b..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/GraphManagementSidebar.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "data-import": "Data import", - "import-task": "Import task", - "map-templates": "Mapping template" -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/addition.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/addition.json deleted file mode 100644 index ebbfa2d81..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/addition.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "addition": { - "function-parameter": { - "edge-type": "Edge Type", - "vertex-id": "Vertex ID" - }, - "store": { - "required": "Required", - "item-is-required": "This item is required", - "no-match-input-requirements": "Does not match input requirements", - "rule1": "Please enter letters, numbers, or special characters", - "rule2": "Please enter a number in the range of 1-65535", - "rule3": "Username and password must be filled in at the same time", - "rule4": "Must be in Chinese, English, numbers, and underscores", - "illegal-data-format": "Illegal data format", - "incorrect-time-format": "Incorrect time format", - "cannot-be-empty": "This item cannot be empty", - "cannot-be-empty1": "This item cannot be empty", - "same-edge-name-notice": "Same edge name exists, please enter another name", - "same-vertex-name-notice": "Same vertex name exists, please enter another name", - "same-property-name-notice": "Same property name exists, please enter another name", - "same-index-name-notice": "Same property index name exists, please enter another name", - "network-error": "Network anomaly, please try again later" - }, - "constant": { - "primary-key-id": "Primary Key ID", - "automatic-generation": "Automatic Generation", - "custom-string": "Custom String", - "custom-number": "Custom Number", - "custom-uuid": "Custom UUID", - "greater-than": "Greater Than", - "greater-than-or-equal": "Greater Than or Equal", - "less-than": "Less Than", - "less-than-or-equal": "Less Than or Equal", - "equal": "Equal" - }, - "menu": { - "chart": "Chart", - "table": "Table", - "task-id": "Task ID", - "list-mode": "List Mode", - "chart-mode": "Chart Mode", - "secondary-index": "Secondary Index", - "range-index": "Range Index", - "full-text-index": "Full Text Index", - "type-index": "Type Index", - "base-info": "Basic Information", - "select-reuse-item": "Select Reuse Item", - "confirm-reuse-item": "Confirm Reuse Item", - "complete-reuse": "Complete Reuse" - }, - "vertex": { - "type-detail": "Vertex Type Details", - "edit-type": "Edit Vertex Type", - "using-cannot-delete": "The current vertex type is in use and cannot be deleted.", - "using-cannot-delete-confirm": "The vertex type in use cannot be deleted. Are you sure you want to delete the following unused vertex types?", - "del-vertex-confirm": "Confirm deletion of this vertex type?", - "del-vertex-confirm-again": "Confirm deletion of this vertex type? Deletion is irreversible, please proceed with caution", - "vertex-type-name": "Vertex Type Name", - "vertex-style": "Vertex Style", - "vertex-display-content": "Vertex Display Content", - "select-vertex-display-content-placeholder": "Please select vertex display content", - "create-vertex-type": "Create Vertex Type", - "vertex-index": "Vertex Index", - "corresponding-vertex-type": "Corresponding Vertex Type", - "no-vertex-type-desc": "You currently do not have any vertex types, create one now" - }, - "edge": { - "display-content": "Edge Display Content", - "display-content-select-desc": "Please select edge display content", - "index-info": "Index Information", - "index-name": "Index Name", - "index-type": "Index Type", - "edge-index": "Edge Index", - "index-type-select-desc": "Please select index type", - "property-select-desc": "Please select property", - "add-group": "Add a group", - "confirm-del-edge-type": "Confirm deletion of this edge type?", - "confirm-del-edge-type-again": "Confirm deletion of edge type? Deletion is irreversible, please proceed with caution", - "confirm-del-edge-careful-notice": "Deletion is irreversible, please proceed with caution", - "no-edge-desc": "You currently do not have any edge types, create one now", - "create-edge": "Create Edge Type", - "multiplexing-existing-type": "Multiplex Existing Type", - "multiplexing-edge-type": "Multiplex Edge Type", - "multiplexing-edge-type-notice": "The attributes and attribute indexes, start and end types, and associated attributes and attribute indexes of the edge type will be reused together", - "verification-result": "Verification Result", - "be-verified": "To be verified", - "verified-again": "Re-verify" - }, - "message": { - "no-can-delete-vertex-type": "No deletable vertex types", - "no-index-notice": "You currently do not have any indexes", - "property-create-desc": "You currently do not have any properties, create one now", - "del-unused-property-notice": "In-use properties cannot be deleted. Confirm deletion of the following unused properties?", - "please-enter-keywords": "Please enter search keywords", - "no-property-can-delete": "No deletable properties", - "no-metadata-notice": "You currently do not have any metadata", - "no-vertex-or-edge-notice": "You have not set any vertex types or edge types", - "no-adjacency-points": "No adjacent points", - "no-more-points": "No more adjacent points", - "please-enter-number": "Please enter a number", - "please-enter-string": "Please enter a string", - "please-enter": "Please enter", - "no-chart-desc": "No graph results, please view the table or JSON data", - "no-data-desc": "No data results at the moment", - "data-loading": "Data loading", - "submit-async-task": "Submitting asynchronous task", - "submit-success": "Submission successful", - "submit-fail": "Submission failed", - "task-submit-fail": "Task submission failed", - "fail-reason": "Failure reason", - "fail-position": "Failure position", - "selected": "Selected", - "edge-del-confirm": "Confirm deletion of the following edges?", - "long-time-notice": "Deleting metadata may take a long time, details can be viewed in task management", - "index-long-time-notice": "Creating indexes may take a long time, details can be viewed in task management", - "property-del-confirm": "Confirm deletion of this property?", - "property-del-confirm-again": "Confirm deletion of this property? Deletion is irreversible, please proceed with caution", - "index-del-confirm": "After deleting the index, queries based on this property index will not be possible. Proceed with caution.", - "edge-name-rule": "Allowing Chinese, English, numbers, and underscores", - "source-type-select-placeholder": "Please select source type", - "target-type-select-placeholder": "Please select target type", - "select-distinguishing-key-property-placeholder": "Please select distinguishing key property", - "select-association-key-property-placeholder": "Please select associated property", - "index-open-notice": "Enabling indexes may affect performance, enable as needed", - "duplicate-name": "Duplicate name", - "dependency-conflict": "Dependency conflict", - "already-exist": "Already exists", - "pass": "Pass", - "select-reuse-graph-placeholder": "Please select the graph to reuse", - "reuse-complete": "Reuse complete", - "vertex-type-reuse-success": "Vertex type reuse successful", - "property-using-cannot-delete": "The current property data is in use and cannot be deleted.", - "reuse-property-success": "Property reuse successful", - "reuse-vertex-type-notice": "The attributes and attribute indexes associated with the vertex type will be reused together", - "illegal-vertex": "This vertex is an illegal vertex, possibly caused by a dangling edge" - }, - "operate": { - "reuse-vertex-type": "Reuse Vertex Type", - "reuse-existing-property": "Reuse Existing Property", - "reuse-property": "Reuse Property", - "create-property": "Create Property", - "continue-reuse": "Continue Reuse", - "back-to-view": "Back to View", - "complete": "Complete", - "next-step": "Next Step", - "previous-step": "Previous Step", - "rename": "Rename", - "del-ing": "Deleting", - "view-task-management": "View Task Management", - "batch-del": "Batch Delete", - "multiplexing": "Multiplex", - "de-multiplexing": "Cancel Multiplexing", - "open": "Open", - "close": "Close", - "look": "View", - "view-property": "View Property", - "filter": "Filter", - "add-filter-item": "Add Property Filter", - "enlarge": "Enlarge", - "narrow": "Shrink", - "center": "Center", - "download": "Download", - "exit-full-screen": "Exit Full Screen", - "full-screen": "Full Screen", - "load-background": "Load Background", - "load-spinner": "Load Spinner", - "rendering": "Rendering", - "detail": "Detail", - "favorite": "Favorite", - "favorite-success": "Favorite successful", - "load-statement": "Load Statement", - "time": "Time", - "name": "Name", - "favorite-statement": "Favorite Statement", - "favorite-desc": "Please enter favorite name", - "operate": "Operate", - "query": "Query", - "hidden": "Hide", - "modify-name": "Modify Name", - "execution-record": "Execution Record", - "favorite-queries": "Favorite Queries", - "favorite-search-desc": "Search favorite name or statement", - "expand-collapse": "Expand/Collapse", - "expand": "Expand", - "collapse": "Collapse", - "favorite-del-desc": "Are you sure you want to delete this favorite statement?", - "input-query-statement": "Please input query statement", - "query-statement-required": "Query statement cannot be empty", - "execute-query": "Execute Query", - "execute-task": "Execute Task", - "execute-ing": "Executing", - "clean": "Clear", - "modify-success": "Modification successful", - "query-result-desc": "Query mode is suitable for small-scale analysis with results returned within 30 seconds; task mode is suitable for large-scale analysis with longer result return times, task details can be viewed in task management" - }, - "common": { - "in-use": "In Use", - "not-used": "Not Used", - "status": "Status", - "no-result": "No Result", - "cardinal-number": "Cardinal Number", - "allow-null": "Allow Null", - "allow-multiple-connections": "Allow Multiple Connections", - "multiple-connections-notice": "Enabling this allows multiple edges of the same type between two vertices", - "term": "Items", - "in-edge": "In Edge", - "add-in-edge": "Add In Edge", - "out-edge": "Out Edge", - "add-out-edge": "Add Out Edge", - "add": "Add", - "value": "Value", - "null-value": "Null Value", - "id-strategy": "ID Strategy", - "id-value": "ID Value", - "id-input-desc": "Please enter ID value", - "please-input": "Please enter", - "add-success": "Add successful", - "add-fail": "Add failed", - "vertex-type": "Vertex Type", - "selected-vertex-type": "Selected Vertex Type", - "vertex-id": "Vertex ID", - "vertex-name": "Vertex Name", - "illegal-vertex-desc": "This vertex is an illegal vertex, possibly caused by a dangling edge", - "add-vertex": "Add Vertex", - "vertex-type-select-desc": "Please select vertex type", - "edge-type": "Edge Type", - "selected-edge-type": "Selected Edge Type", - "edge-style": "Edge Style", - "modify-edge-type": "Edit Edge Type", - "edge-type-detail": "Edge Type Details", - "edge-type-name": "Edge Type Name", - "edge-direction": "Edge Direction", - "edge-type-select-desc": "Please select edge type", - "edge-id": "Edge ID", - "edge-name": "Edge Name", - "source": "Source", - "source-type": "Source Type", - "target": "Target", - "target-type": "Target Type", - "rule": "Rule", - "property": "Property", - "selected-property": "Selected Property", - "property-name": "Property Name", - "add-property": "Add Property", - "association-property": "Association Property", - "association-property-and-type": "Association Property and Type", - "property-index": "Property Index", - "selected-property-index": "Selected Property Index", - "property-index-name": "Property Index Name", - "property-value": "Property Value", - "required-property": "Required Property", - "nullable-property": "Nullable Property", - "primary-key": "Primary Key", - "primary-key-property": "Primary Key Property", - "select-primary-key-property-placeholder": "Please select primary key property", - "distinguishing-key": "Distinguishing Key", - "distinguishing-key-property": "Distinguishing Key Property", - "property-input-desc": "Please enter property value", - "del-comfirm": "Confirm deletion", - "ask": "?", - "confirm": "Confirm", - "del-success": "Deletion successful", - "save-scuccess": "Save successful", - "save-fail": "Save failed", - "save": "Save", - "cancel": "Cancel", - "more": "More", - "edit": "Edit", - "del": "Delete", - "fold": "Fold", - "open": "Expand", - "close": "Close", - "required": "Required", - "format-error-desc": "Must start with a letter, allow English, numbers, underscore", - "no-matching-results": "No matching results", - "no-data": "No data", - "data-type": "Data Type", - "corresponding-type": "Corresponding Type" - }, - "appbar": { - "graph-manager": "Graph Manager" - }, - "graphManagementHeader": { - "graph-manager": "Graph Manager", - "community": "Community Edition", - "business": "Business Edition", - "limit-desc": "Graph Limit Support", - "limit-desc1": "Graph Disk Limit", - "individual": "Individual", - "input-placeholder": "Search for graph name or ID", - "graph-create": "Create Graph" - }, - "graphManagementList": { - "save-scuccess": "Save successful", - "graph-del": "Delete Graph", - "graph-del-comfirm-msg": "Once deleted, all configurations for this graph cannot be restored", - "graph-edit": "Edit Graph", - "id": "Graph ID", - "id-desc": "Set a unique identifier for the created graph", - "name": "Graph Name", - "name-desc": "Enter the name of the graph to connect to", - "host": "Hostname", - "port": "Port Number", - "port-desc": "Please enter port number", - "username": "Username", - "password": "Password", - "creation-time": "Creation Time", - "visit": "Visit" - }, - "graphManagementEmptyList": { - "graph-create": "Create Graph", - "graph-create-desc": "You currently do not have any graphs, create one now", - "no-matching-results": "No matching results" - }, - "newGraphConfig": { - "graph-create": "Create Graph", - "create": "Create", - "create-scuccess": "Create successful", - "id": "Graph ID", - "id-desc": "Set a unique identifier for the created graph", - "name": "Graph Name", - "name-desc": "Enter the name of the graph to connect to", - "host": "Hostname", - "host-desc": "Please enter hostname", - "port": "Port Number", - "port-desc": "Please enter port number", - "username": "Username", - "password": "Password", - "not-required-desc": "Leave blank if not required" - }, - "graphManagementSidebar": { - "data-analysis": "Data Analysis", - "graph-select": "Select Graph", - "metadata-config": "Metadata Configuration", - "data-import": "Data Import", - "task-management": "Task Management" - }, - "dataAnalyze": { - "cannot-access": "Cannot Access", - "return-home": "Return Home" - }, - "dataAnalyzeInfoDrawer": { - "edit-details": "Edit Details", - "data-details": "Data Details" - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/common.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/common.json deleted file mode 100644 index f90df6586..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/common.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "common": { - "loading-data": "Data loading" - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/data-import/import-tasks/ImportTasks.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/data-import/import-tasks/ImportTasks.json deleted file mode 100644 index d7039389b..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/data-import/import-tasks/ImportTasks.json +++ /dev/null @@ -1,343 +0,0 @@ -{ - "breadcrumb": { - "first": "import task", - "second": "Task Details" - }, - "import-manager": { - "hint": { - "empty-task": "you have no tasks yet, create one now", - "no-result": "no results", - "creation-succeed": "Creation Succeeded", - "update-succeed": "Edit Succeeded" - }, - "manipulation": { - "create": "create" - }, - "placeholder": { - "input-job-name": "Please Enter Task so to search", - "input-valid-job-name": "Allow is Chinese, English, Numbers, Underscores", - "input-job-description": "Please Enter Task Description" - }, - "list-column-title": { - "job-name": "task name", - "size": "size", - "create-time": "creation time", - "status": "status", - "time-consuming": "time consumed", - "manipulation": "operation" - }, - "list-column-status": { - "DEFAULT": "not started", - "UPLOADING": "not started", - "MAPPING": "setting", - "SETTING": "Importing", - "LOADING": "Importing", - "FAILED": "failed", - "SUCCESS": "succeeded" - }, - "list-column-manipulations": { - "start": "Start Task", - "resume-setting": "constinue setting", - "resume-importing": "continue improTing", - "check-error-log": "check reaSon", - "delete": "delete" - }, - "modal": { - "create-job": { - "title": "Create Import Task", - "job-name": "task name:", - "job-description": "task description:" - }, - "delete-job": { - "title": "delete task", - "hint": "Confirm delete task {{So}}?", - "sub-hint": "This task cannot be recovered after deletion" - }, - "manipulations": { - "create": "create", - "delete": "delete", - "cancel": "cancel" - } - }, - "validator": { - "no-empty": "this fire is required", - "over-limit-size": "Exceeed Character Limit", - "invalid-format": "Please Enter Chinese, English, Numbers, Underscores" - } - }, - "import-job-details": { - "tabs": { - "basic-settings": "Basic Settings", - "uploaded-files": "uploaded files", - "data-maps": "MAPPING FILES", - "import-details": "Import Details" - }, - "basic": { - "job-name": "task name:", - "job-description": "task description:", - "modal": { - "edit-job": { - "title": "Edit Import Task", - "job-name": "task name:", - "job-description": "task description:" - }, - "manipulations": { - "save": "save", - "cancel": "cancel" - } - } - }, - "manipulations": { - "edit": "edit", - "resume-task": "resume task" - } - }, - "step": { - "first": "upload files", - "second": "set data mapping", - "third": "import data", - "fourth": "complete" - }, - "upload-files": { - "click": "CLICK", - "description": "Click or Drag Files Hereto Upload, Multiple CSV Files Supported, Single File Up TO 1GB, Total up to 10GB", - "drag": "Drag", - "description-1": "OR DRAG FILES", - "description-2": "Here to upload, multiple CSV Files SUPPORTED, Single File up to 1GB, Total up to 10GBBGB", - "cancel": "Cancel Upload", - "next": "next", - "wrong-format": "ONLY CSV Format Files Are Supported", - "over-single-size-limit": "SIZE Exceeds 1 GB", - "over-all-size-limit": "Total Size Exceeds 10 GB", - "empty-file": "file is empty, please re-upload", - "no-duplicate": "the footowing uploaded files already exist:" - }, - "data-configs": { - "file": { - "title": "file settings", - "include-header": "Include Header", - "delimiter": { - "title": "delimiter", - "comma": "comma", - "semicolon": "semicolon", - "tab": "tab", - "space": "space", - "custom": "custom" - }, - "code-type": { - "title": "encoding format", - "UTF-8": "UTF-8", - "GBK": "GBK", - "ISO-8859-1": "ISO-8859-1", - "US-ASCII": "US-Ascii", - "custom": "custom" - }, - "date-type": { - "title": "date format", - "custom": "custom" - }, - "skipped-line": "skipped line", - "timezone": "Timezone", - "save": "save", - "placeholder": { - "input-delimiter": "Please Enter Delimiter", - "input-charset": "Please Enter Encoding Format", - "input-date-format": "Please Enter date format" - }, - "hint": { - "save-succeed": "file settings saved" - } - }, - "type": { - "title": "TypeSettings", - "basic-settings": "Basic Settings", - "manipulation": { - "create": "create", - "save": "save", - "cancel": "cancel", - "create-vertex": "create vertex mapping", - "create-edge": "Create EDGE MAPPING" - }, - "info": { - "type": "Type", - "name": "name", - "ID-strategy": "Id strategy", - "edit": "edit", - "delete": "delete" - }, - "ID-strategy": { - "PRIMARY_KEY": "Primary Key ID", - "AUTOMATIC": "Automaticly Generated", - "CUSTOMIZE_STRING": "CUSTOM String", - "CUSTOMIZE_NUMBER": "CUSTOM NUMBER", - "CUSTOMIZE_UUID": "CUSTOM UUID" - }, - "vertex": { - "title": "create vertex mapping", - "type": "vertex type", - "ID-strategy": "Id strategy", - "ID-column": "Id column", - "map-settings": "MAPPING SETTINGS", - "add-map": { - "title": "add maping", - "name": "column name", - "sample": "Data Sample", - "property": "MAPPING PROPERTY", - "clear": "clear" - }, - "select-all": "Select all", - "advance": { - "title": "Advanced Settings", - "nullable-list": { - "title": "null able list", - "empty": "Empty Value", - "custom": "custom" - }, - "map-property-value": { - "title": "Property Value Mapping", - "add-value": "Add Property Value Mapping", - "fields": { - "property": "Property", - "value-map": "Value Map", - "add-value-map": "add value map" - } - }, - "placeholder": { - "input": "Please Select", - "input-property": "Please Select input Property", - "input-file-value": "Please Enter File Value", - "input-graph-value": "Please Enter Graph Import Value" - } - } - }, - "edge": { - "title": "Create EDGE MAPPING", - "type": "EDGE TYPE", - "source-ID-strategy": "source ID Strategy", - "target-ID-strategy": "target ID Strategy", - "ID-column": "Id column", - "map-settings": "MAPPING SETTINGS", - "add-map": { - "title": "add maping", - "name": "column name", - "sample": "Data Sample", - "property": "MAPPING PROPERTY", - "clear": "clear" - }, - "select-all": "Select all", - "advance": { - "title": "Advanced Settings", - "nullable-list": { - "title": "null able list", - "empty": "Empty Value", - "custom": "custom" - }, - "map-property-value": { - "title": "Property Value Mapping", - "add-value": "Add Property Value Mapping", - "fields": { - "property": "Property", - "value-map": "Value Map", - "add-value-map": "add value map" - } - }, - "placeholder": { - "input": "Please Select Mapping Property", - "input-property": "Please Select input Property", - "input-file-value": "Please Enter File Value", - "input-graph-value": "Please Enter Graph Import Value" - } - } - }, - "hint": { - "lack-support-for-automatic": "Automaticly Generated ID Strategy Does Not Support Visual Import, Please Call API", - "no-vertex-or-edge-mapping": "The Follow Files have not set verstex or edge type mapping:" - }, - "placeholder": { - "select-vertex-type": "Please Select vertex Type", - "select-edge-type": "Please selectd a Type", - "select-id-column": "Please Select ID Column", - "empty-value": "empty" - } - }, - "manipulations": { - "previous": "previous", - "next": "next", - "add": "ADD", - "edit": "edit", - "delete": "delete", - "cancel": "cancel", - "hints": { - "delete-confirm": "Confirm delete?", - "warning": "Cannot be recovered after deletion, please openly carefully" - } - }, - "validator": { - "no-empty": "this fire cannot be empty" - } - }, - "server-data-import": { - "import-settings": { - "title": "import settings", - "checkIfExist": "Check if version connected by edest", - "requestTimesWhenInterpolationFailed": "Retry Times When Insertion Fails", - "maximumAnalyzedErrorRow": "maximum Allowed Parsing Error ROWS", - "requestTicksWhenInterpolationFailed": "Retry Interval for Insertion Failures/Yes", - "maxiumInterpolateErrorRow": "maximum Allowed Insertion Error ROWS", - "InterpolationTimeout": "INSERTION TIMEOUT/" - }, - "import-details": { - "title": "Import Details", - "column-titles": { - "file-name": "filename", - "type": "Type", - "import-speed": "Import Speed", - "import-progress": "Import Progress", - "status": "status", - "time-consumed": "time consumed", - "manipulations": "operations" - }, - "content": { - "vertex": "vertex", - "edge": "EDGE" - }, - "status": { - "RUNNING": "running", - "SUCCEED": "succeeded", - "FAILED": "failed", - "PAUSED": "paused", - "STOPPED": "stopped" - }, - "manipulations": { - "pause": "pause", - "resume": "resume", - "retry": "Retry", - "abort": "abort", - "failed-cause": "check reaSon" - } - }, - "hint": { - "check-vertex": "enabling check is Will Affect Import Performance, Enable As Needed", - "no-data": "requesting import", - "confirm-navigation": "Confirm navigation to task list?" - }, - "validator": { - "no-empty": "this fire cannot be empty", - "need-integer-with-negative": "Please Enter -1 or Dark Integer Greater than 0", - "need-integer": "Please Enter press Integer Greater than 0" - }, - "manipulations": { - "previous": "previous", - "start": "Start Import", - "cancel": "Cancel Import", - "finished": "Finished" - } - }, - "data-import-status": { - "finished": "Import Completed", - "success": "SuccessFully Importing {{number}} Files", - "pause": "{{number}} Files Importing Pause", - "abort": "{{number}} Files Importing Abort", - "move-to-import-manager": "back to import manager" - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/dataAnalyze.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/dataAnalyze.json deleted file mode 100644 index 6eaadf241..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/dataAnalyze.json +++ /dev/null @@ -1,654 +0,0 @@ -{ - "data-analyze": { - "category": { - "gremlin-analyze": "Gremlin analysis", - "algorithm-analyze": "Algorithm analysis" - }, - "manipulations": { - "execution": "implement", - "favorite": "collect", - "reset": "Repossess" - }, - "hint": { - "graph-disabled": "This figure is not available" - }, - "algorithm-list": { - "title": "Algorithm directory", - "loop-detection": "Ring detection", - "focus-detection": "Intersection detection", - "shortest-path": "Minimum path", - "shortest-path-all": "The shortest path", - "all-path": "All paths", - "model-similarity": "Model similarity algorithm", - "neighbor-rank": "Neighbor Rank recommendation algorithm", - "k-step-neighbor": "K -step neighbor", - "k-hop": "K jump algorithm", - "custom-path": "Custom path", - "radiographic-inspection": "Ray detection", - "same-neighbor": "Co -neighbor", - "weighted-shortest-path": "The shortest path with power", - "single-source-weighted-shortest-path": "The shortest path of single source band power", - "jaccard": "Jaccard similarity", - "personal-rank": "Personal Rank recommendation algorithm" - }, - "algorithm-forms": { - "loop-detection": { - "options": { - "source": "Starting point ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "source_in_ring": "The ring contains the starting point:", - "limit": "Back to the maximum value of the path:", - "capacity": "The maximum value of the access to the vertex:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "input-positive-integer": "Please fill in an integer greater than 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "focus-detection": { - "options": { - "source": "Starting point ID:", - "target": "End ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "capacity": "The maximum value of the access to the vertex:", - "limit": "The maximum value of the return point:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-target-id": "Please enter the end ID", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "input-positive-integer": "Please fill in an integer greater than 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "shortest-path": { - "options": { - "source": "Starting point ID:", - "target": "End ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "skip_degree": "Super vertex degree:", - "capacity": "The maximum value of the access to the vertex:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-target-id": "Please enter the end ID", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-integer": "Please fill in an integer that is greater than or equal to 0, and defaults to 0", - "input-positive-integer": "Please fill in an integer greater than 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex", - "skip-degree": "Fill in the minimum number of edges that need to be skipped during the query process, that is, when the number of edges of the vertex is greater than the super -vertex degree, skipping this vertex can be used to avoid the super point" - }, - "validations": { - "no-empty": "This item cannot be empty", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0", - "integer-only": "Please fill in an integer more than or equal to 0", - "postive-integer-only": "Please fill in an integer greater than 0" - } - }, - "shortest-path-all": { - "options": { - "source": "Starting point ID:", - "target": "End ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "capacity": "The maximum value of the access to the vertex:", - "skip_degree": "Super vertex degree:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-target-id": "Please enter the end ID", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer": "Please fill in an integer greater than 0", - "input-integer": "Please fill in an integer that is greater than or equal to 0, and defaults to 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex", - "skip-degree": "Fill in the minimum number of edges that need to be skipped during the query process, that is, when the number of edges of the vertex is greater than the super -vertex degree, skipping this vertex can be used to avoid the super point" - }, - "validations": { - "no-empty": "This item cannot be empty", - "integer-only": "Please fill in an integer more than or equal to 0", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "all-path": { - "options": { - "source": "Starting point ID:", - "target": "End ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "capacity": "The maximum value of the access to the vertex:", - "limit": "The maximum value of the return path:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-target-id": "Please enter the end ID", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "input-positive-integer": "Please fill in an integer greater than 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "model-similarity": { - "options": { - "method": "Starting point selection method:", - "source": "Starting point ID:", - "vertex-type": "Sperture type:", - "vertex-property": "Speak attributes and values:", - "direction": "direction:", - "least_neighbor": "Number of neighbors:", - "similarity": "Similarity:", - "label": "Border type:", - "max_similar": "The highest degree of similarity:", - "least_similar": "The minimum number of the model is similar:", - "property_filter": "Attribute filtering:", - "least_property_number": "Number of minimum attribute values:", - "max_degree": "The maximum degree:", - "capacity": "The maximum value of the access to the vertex:", - "skip_degree": "Back to the highest value of the vertex:", - "limit": "The maximum value of the return result:", - "return_common_connection": "Return to the common point of association:", - "return_complete_info": "Return to the vertex complete information:" - }, - "radio-value": { - "specific-id": "Specify ID", - "filtered-type-property": "Filter type attribute" - }, - "placeholder": { - "input-source-id": "Please enter the starting point ID, multiple IDs are separated by comma", - "input-vertex-type": "Please select vertex type", - "select-vertex-property": "Please select vertex attributes", - "input-vertex-property": "Multi -attribute value is separated by comma", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "input-integer": "Please fill in an integer more than or equal to 0", - "input-positive-integer": "Please fill in an integer greater than 0", - "input-integer-gt-1": "Please fill in an integer greater than 1", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0", - "input-filtered-property": "Please select the attribute you need to filter", - "no-properties": "No attribute", - "no-vertex-type": "No -free type", - "similarity": "Please enter the number of (0-1]" - }, - "hint": { - "vertex_type_or_property": "Sperture type/vertex attribute to at least one item", - "least_property_number": "The number of attribute filtering and minimum attribute values ​​must be used together; after setting, the effect is: when the starting point is similar to that of the value of a certain attribute value of the minimum attribute value, the starting point and its shuttle shape will be returned.Similar point", - "max-degree": "During the query, the maximum number of edges of a single vertex", - "least_neighbor": "The number of neighbors is less than the current setting value, then it is thought that there is no shuttle similar point from the starting point", - "similarity": "The proportion of the number of neighbors that starts from the starting point to the \"Similar Point\" of the \"shuttle -shaped\"", - "max_similar": "The number of TOP with the highest degree of similarity in the shuttle point of the starting point, 0 means all", - "return_common_connection": "Whether to return the starting point and its \"shuttle -shaped similar point\" in the middle point" - }, - "validations": { - "no-empty": "This item cannot be empty", - "no-edge-typs": "Infinite type", - "integer-only": "Please fill in an integer more than or equal to 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0", - "postive-integer-only": "Please fill in an integer greater than 0", - "integer-gt-1": "Please fill in an integer greater than 1", - "similarity": "Please enter the number of (0-1]", - "no-gt-1000": "This value cannot be greater than equal to 1000" - }, - "add": "Add to", - "delete": "delete", - "pre-value": "all" - }, - "neighbor-rank": { - "options": { - "source": "Starting point ID:", - "alpha": "alpha:", - "direction": "direction:", - "capacity": "The maximum value of the access to the vertex:", - "label": "Border type:", - "degree": "The maximum degree:", - "top": "Top n: each layer retain weight top n:", - "steps": "steps:" - }, - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-integer-lt-1000": "Please fill in an integer greater than or equal to 0, less than 1000, and default to 100", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer": "Please fill in an integer greater than 0", - "range": "Scope (0-1]" - }, - "hint": { - "top": "In each layer, there are n results with only the highest weight of the rights in the result", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "no-edge-typs": "Infinite type", - "range": "Please fill in the value greater than 0 and less than equal to 1", - "integer-only-lt-1000": "Please fill in an integer with an integer greater than equal to 0 less than 1000", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - }, - "pre-value": "all", - "add-new-rule": "Add rules" - }, - "k-step-neighbor": { - "options": { - "source": "Starting point ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "limit": "Back to the highest value of the vertex:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "input-positive-integer": "Please fill in an integer greater than 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "k-hop": { - "options": { - "source": "Starting point ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "nearest": "The shortest path:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "capacity": "The maximum value of access to the vertex during traversal:", - "limit": "Back to the highest value of the vertex:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "input-positive-integer": "Please fill in an integer greater than 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex", - "shortest-path": "After turning on, check the shortest path of the starting point of the starting point is the vertex of the DEPTH step. After the closure, you will query the vertex of the starting point of the starting point of the DEPTH step." - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "custom-path": { - "options": { - "method": "Starting point selection method:", - "source": "Starting point ID:", - "vertex-type": "Sperture type:", - "vertex-property": "Speeton attribute:", - "sort_by": "Path weight sorting:", - "capacity": "The maximum value of access to the vertex during traversal:", - "limit": "Back to the highest value of the vertex:", - "direction": "direction:", - "labels": "Border type:", - "properties": "Border attribute:", - "weight_by": "Calculate the edge weight according to the attribute:", - "degree": "The maximum degree:", - "sample": "Sample value:", - "steps": "steps:" - }, - "placeholder": { - "input-source-id": "Please enter the starting point ID, multiple IDs are separated by comma", - "select-vertex-type": "Please select vertex type", - "select-vertex-property": "Please select vertex attributes", - "input-multiple-properties": "Multi -attribute value is separated by comma", - "input-integer": "Please fill in an integer more than or equal to 0", - "input-positive-integer": "Please fill in an integer greater than 0", - "input-positive-integer-or-negative-one-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "input-property": "Please enter the edge attribute", - "input-number": "Please enter the floating point number", - "select-edge-type": "Please select the side type", - "select-edge-property": "Please select the edge attribute", - "select-property": "Please select the attribute", - "no-vertex-type": "No -free type", - "no-vertex-property": "Non -point attribute", - "no-edge-type": "Infinite type", - "no-edge-property": "Infinite attribute", - "no-properties": "No attribute" - }, - "hint": { - "top": "In each layer, there are n results with only the highest weight of the rights in the result", - "vertex_type_or_property": "Sperture type/vertex attribute to at least one item" - }, - "radio-value": { - "specific-id": "Specify ID", - "filtered-type-property": "Filter type attribute", - "none": "Not sort", - "ascend": "Sequence", - "descend": "Order" - }, - "validations": { - "no-empty": "This item cannot be empty", - "no-edge-typs": "Infinite type", - "range": "Please fill in the value greater than 0 and less than equal to 1", - "integer-only": "Please fill in an integer more than or equal to 0", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0", - "input-number": "Please enter the floating point number" - }, - "custom-weight": "Customized rights value", - "add": "Add to", - "delete": "delete", - "add-new-rule": "Add rules" - }, - "radiographic-inspection": { - "options": { - "source": "Starting point ID:", - "direction": "direction:", - "max_depth": "Maximum step:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "capacity": "The maximum value of access to the vertex during traversal:", - "limit": "Back to the maximum value of the non -ring road path:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-positive-integer": "Please fill in an integer greater than 0", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "same-neighbor": { - "options": { - "vertex": "Speed ​​1:", - "other": "Sperture 2:", - "direction": "direction:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "limit": "Back to the maximum value of the joint neighbor:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the vertex ID", - "input-other-id": "Please enter the ID different from vertex 1", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer": "Please fill in an integer greater than 0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0", - "no-same-value-with-other": "Can't be the same as Sperture 2", - "no-same-value-with-vertex": "Can't be the same as Sperture 1" - } - }, - "weighted-shortest-path": { - "options": { - "source": "Starting point ID:", - "target": "End ID:", - "direction": "direction:", - "weight": "Holding attributes:", - "with_vertex": "Return to the vertex complete information:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "skip_degree": "The degree of the jump point:", - "capacity": "The maximum value of access to the vertex during traversal:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-target-id": "Please enter the end ID", - "input-integer": "Please fill in an integer more than or equal to 0", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "select-property": "Please select the attribute", - "input-positive-integer": "Please fill in an integer greater than 0, default to 0", - "no-property": "No attribute value to digital type attributes", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "skip-degree": "When the number of edges of the vertex is greater than the filling value, skip the current vertic", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "integer-only": "Please fill in an integer more than or equal to 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0", - "postive-integer-only": "Please fill in an integer greater than 0" - } - }, - "single-source-weighted-shortest-path": { - "options": { - "source": "Starting point ID:", - "direction": "direction:", - "weight": "Holding attributes:", - "with_vertex": "Return to the vertex complete information:", - "label": "Border type:", - "max_degree": "The maximum degree:", - "skip_degree": "The degree of the jump point:", - "capacity": "The maximum value of access to the vertex during traversal:", - "limit": "Back to the vertex/shortest path maximum value:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-integer": "Please fill in an integer that is greater than or equal to 0, and defaults to 0", - "input-positive-integer": "Please fill in an integer greater than 0", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-capacity": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, and default to 10", - "no-property": "If you do not fill in the weight of 1.0", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "skip-degree": "When the number of edges of the vertex is greater than the filling value, skip the current vertic", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "integer-only": "Please fill in an integer more than or equal to 0", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - } - }, - "jaccard": { - "options": { - "vertex": "Speed ​​1:", - "other": "Sperture 2:", - "direction": "direction:", - "label": "Border type:", - "max_degree": "The maximum degree:" - }, - "pre-value": "all", - "placeholder": { - "input-source-id": "Please enter the vertex ID", - "input-other-id": "Please enter the ID different from vertex 1", - "input-positive-integer": "Please fill in an integer greater than 0", - "input-positive-integer-or-negative-one-max-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "no-edge-types": "Infinite type" - }, - "hint": { - "max-depth": "In order to ensure performance, it is recommended not to exceed 10 steps. Recommend 5 steps", - "max-degree": "During the query, the maximum number of edges of a single vertex" - }, - "validations": { - "no-empty": "This item cannot be empty", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0", - "no-same-value-with-other": "Can't be the same as Sperture 2", - "no-same-value-with-vertex": "Can't be the same as Sperture 1" - } - }, - "personal-rank": { - "options": { - "source": "Starting point ID:", - "alpha": "alpha:", - "max_depth": "Number of iteration:", - "with_label": "Back results screening:", - "label": "Border type:", - "degree": "The maximum degree:", - "limit": "Back to the highest value of the vertex:", - "sorted": "Return results sorting:" - }, - "with-label-radio-value": { - "same_label": "Same type vertex", - "other_label": "Different types of vertices", - "both_label": "All type vertices" - }, - "placeholder": { - "input-source-id": "Please enter the starting point ID", - "input-positive-integer-or-negative-one-degree": "Please fill in an integer of -1 or greater than 0, the default is 10000", - "input-positive-integer-or-negative-one-limit": "Please fill in an integer of -1 or greater than 0, the default is 10000,000", - "select-edge": "Please select the side type", - "input-positive-integer": "Please fill in an integer greater than 0", - "alpha": "Please enter the number of (0-1]", - "max_depth": "Please enter the number of (0-50]" - }, - "hint": { - "degree": "During the query, the maximum number of edges of a single vertex", - "with-label": "Depending on whether it is the same as the starting point type, screen the return result", - "sorted": "Choose to open, then arrange the order, choose the level, but not sort" - }, - "validations": { - "no-empty": "This item cannot be empty", - "no-edge-typs": "Infinite type", - "alpha-range": "Please fill in the value greater than 0 and less than equal to 1", - "depth-range": "Please fill in the value of greater than 0 and less than equal to 50", - "postive-integer-only": "Please fill in an integer greater than 0", - "positive-integer-or-negative-one-only": "Please fill in an integer of -1 or greater than 0" - }, - "pre-value": "all", - "add-new-rule": "Add rules" - }, - "api-name-mapping": { - "rings": "Ring detection", - "crosspoints": "Intersection detection", - "shortpath": "Minimum path", - "allshortpath": "The shortest path", - "paths": "All paths", - "fsimilarity": "Model similarity algorithm", - "neighborrank": "Neighbor Rank recommendation algorithm", - "kneighbor": "K -step neighbor", - "kout": "K jump algorithm", - "customizedpaths": "Custom path", - "rays": "Ray detection", - "sameneighbors": "Co -neighbor", - "weightedshortpath": "The shortest path with power", - "singleshortpath": "The shortest path of single source band power", - "jaccardsimilarity": "Jaccard similarity", - "personalrank": "Personal Rank recommendation algorithm" - } - }, - "exec-logs": { - "table-title": { - "time": "time", - "type": "Execute type", - "content": "Execute content", - "status": "state", - "duration": "time consuming", - "manipulation": "operate" - }, - "type": { - "GREMLIN": "Gremlin query", - "GREMLIN_ASYNC": "GREMLIN task", - "ALGORITHM": "Algorithm query" - }, - "status": { - "success": "success", - "async-success": "Successful submission", - "running": "In operation", - "failed": "fail", - "async-failed": "Submit failure" - } - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/index.ts b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/index.ts deleted file mode 100644 index b3e03211e..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/graph-managment/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import CommonResources from './common.json'; -import GraphManagementSideBarResources from './GraphManagementSidebar.json'; -import DataAnalyzeResources from './dataAnalyze.json'; -import DataImportResources from './data-import/import-tasks/ImportTasks.json'; -import AsyncTasksResources from './AsyncTasks.json'; -import Addition from './addition.json'; - -export { - CommonResources, - DataAnalyzeResources, - GraphManagementSideBarResources, - DataImportResources, - AsyncTasksResources, - Addition -}; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/index.js b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/index.js new file mode 100644 index 000000000..4351084c8 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/index.js @@ -0,0 +1,46 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {merge} from 'lodash'; +import { + Board, + Common, + ERView, +} from './components'; +import { + Home, + Manage, + Analysis, + Modules, + Pages, +} from './modules'; + +const translation = { + translation: merge( + Board, + Common, + Home, + Manage, + ERView, + Analysis, + Modules, + Pages + ), +}; + +export default translation; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/index.ts b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/index.ts deleted file mode 100644 index 4a369f54e..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import { merge } from 'lodash-es'; -import { - CommonResources, - DataAnalyzeResources, - GraphManagementSideBarResources, - DataImportResources, - AsyncTasksResources, - Addition -} from './graph-managment'; - -const translation = { - translation: merge( - CommonResources, - DataAnalyzeResources, - GraphManagementSideBarResources, - DataImportResources, - AsyncTasksResources, - Addition - ) -}; - -export default translation; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json new file mode 100644 index 000000000..084a7505c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json @@ -0,0 +1,860 @@ +{ + "analysis": { + "name": "Business Analysis", + "query": { + "name": "Graph Query", + "placeholder": "Enter a query statement", + "gremlin_tab": "Gremlin", + "cypher_tab": "Cypher", + "text2gql_tab": "Natural language", + "text2gql_badge": "Coming soon", + "text2gql_title": "Natural-language graph query", + "text2gql_description": "This is a UI preview for a future Text2GQL workflow. It is not connected to a model or query service.", + "text2gql_placeholder": "Describe the graph question you want to ask", + "text2gql_privacy": "Nothing entered here is sent, generated, saved, or executed.", + "execute_query": "Run Query", + "execute_task": "Run Task", + "execute_mode_desc": "Query mode is for small analyses that return within 30 seconds. Task mode is for larger analyses that take longer; task details are available in Task Management.", + "empty_query": "Query statement cannot be empty", + "collapse": "Collapse", + "expand": "Expand", + "favorite": "Favorite", + "favorite_statement": "Favorite Statement", + "favorite_name_placeholder": "Enter favorite name", + "favorite_success": "Favorited successfully", + "favorite_failed": "Failed to favorite" + }, + "query_result": { + "graph": "Graph", + "table": "Table", + "no_data": "No data", + "loading": "Loading data...", + "run_failed": "Run failed", + "run_failed_action": "Query failed. Review the statement and try again.", + "submitting_task": "Submitting async task...", + "submit_failed": "Submit failed", + "import_failed": "Import failed", + "submit_success": "Submitted successfully", + "no_graph_result": "No graph result. Check Table or Json data.", + "no_table_result": "No table result. Check Graph or Json data.", + "no_json_result": "No Json result. Check Graph or Table data.", + "expand_failed": "Failed to expand", + "no_more_neighbors": "No more adjacent vertices" + }, + "canvas": { + "import": "Import", + "export": "Export", + "style": "Style", + "filter": "Filter", + "layout": "Layout", + "setting": "Settings", + "new": "New", + "statistics": "Statistics", + "mode_2d": "2D Mode", + "mode_3d": "3D Mode", + "import_success": "Imported successfully", + "export_json": "Export Json", + "export_png": "Export image", + "export_json_title": "Export Json", + "export_image_title": "Export image", + "file_name": "File Name", + "file_name_required": "Enter a file name", + "file_name_placeholder": "Enter a file name", + "add_vertex": "Add Vertex", + "add_in_edge": "Add In Edge", + "add_out_edge": "Add Out Edge", + "toolbar": { + "clear_canvas": "Clear Canvas", + "fit_center": "Fit Center", + "full_screen": "Full Screen", + "undo": "Undo", + "redo": "Redo", + "refresh_layout": "Refresh Layout", + "zoom_out": "Zoom Out", + "zoom_in": "Zoom In", + "fix_node": "Fix Node", + "isolated_nodes": "Isolated Nodes" + }, + "clear_graph": { + "title": "Clear the current canvas?", + "description": "Clear the current canvas. This action cannot be undone.", + "ok": "OK", + "cancel": "Cancel" + }, + "context_menu": { + "expand": "Expand", + "search": "Search", + "fix": "Fix", + "unfix": "Unfix", + "hide": "Hide", + "add_out_edge": "Add Out Edge", + "add_in_edge": "Add In Edge", + "add_vertex": "Add Vertex" + }, + "tooltip": { + "import_2d": "The current graph already has data. Clear it before importing.", + "import_3d": "Import is not supported in 3D mode.", + "export_2d": "No graph data to export.", + "export_3d": "Export is not supported in 3D mode.", + "style_2d": "No graph data to style.", + "style_3d": "Style settings are not supported in 3D mode.", + "filter_2d": "No graph data to filter.", + "filter_3d": "Filter is not supported in 3D mode.", + "layout_2d": "No graph data to lay out.", + "layout_3d": "Layout settings are not supported in 3D mode.", + "setting_2d": "No graph data to configure.", + "setting_3d": "Settings are not supported in 3D mode.", + "new_2d": "No graph data to edit.", + "new_3d": "New graph elements are not supported in 3D mode.", + "statistics_2d": "No graph data to analyze.", + "statistics_3d": "Statistics are not supported in 3D mode.", + "switch_2d": "No graph data to switch." + }, + "canvas_3d": { + "node": "Node", + "edge": "Edge", + "type": "Type", + "id": "ID", + "tip": "Tip:", + "left_mouse": "Left mouse", + "right_mouse": "Right mouse", + "rotate": "rotate", + "move": "move", + "edge_label_type": { + "parent": "Parent edge", + "sub": "Sub edge" + } + }, + "filter_drawer": { + "title": "Query", + "filter": "Filter", + "logic": "Logic", + "filter_type": "Filter Type", + "operation_type": "Operation Type", + "vertex": "Vertex", + "edge": "Edge", + "vertex_property": "Vertex Property", + "vertex_id": "Vertex ID", + "vertex_label": "Vertex Label", + "edge_property": "Edge Property", + "edge_id": "Edge ID", + "edge_label": "Edge Type", + "edge_direction": "Edge Direction", + "property": "Property", + "rule": "Rule", + "value": "Value", + "add_expression": "Add Expression", + "delete_expression": "Delete Expression", + "add_property": "Add Property", + "delete": "Delete", + "duplicate_property": "Property cannot be duplicated", + "get_edge_failed": "Failed to get edge information", + "get_adjacent_edge_failed": "Failed to get adjacent edges", + "number_placeholder": "Enter a number", + "string_placeholder": "Enter a string", + "placeholder": "Enter a value", + "rule_option": { + "gt": "Greater than", + "gte": "Greater than or equal to", + "eq": "Equal to", + "lt": "Less than", + "lte": "Less than or equal to", + "true": "True", + "false": "False" + } + }, + "legend": { + "title": "Legend" + }, + "element_tooltip": { + "node": "Node", + "edge": "Edge", + "type": "Type", + "edge_label_type": { + "parent": "Parent edge", + "sub": "Sub edge", + "normal": "Normal edge" + } + }, + "task_navigation": { + "success_alt": "Submitted successfully", + "task_id": "Task ID", + "view": "View" + }, + "setting_panel": { + "title": "Settings", + "node_id": "Node ID", + "edge_type": "Edge Type", + "node_center": "Node Center", + "node_bottom": "Node Bottom", + "node_label": "Node Label", + "node_label_property": "Node Label Property", + "node_label_position": "Node Label Position", + "node_label_size": "Node Label Font Size", + "edge_label": "Edge Label", + "edge_label_property": "Edge Label Property", + "edge_label_size": "Edge Label Font Size", + "components": "Components", + "enable_minimap": "Enable Minimap", + "enable_grid": "Enable Grid Background", + "enable_legend": "Enable Legend" + }, + "layout_panel": { + "title": "Layout", + "enable_layout": "Enable Layout", + "enable_layout_tooltip": "Enable layout. When disabled, the graph uses the default layout.", + "layout_type": "Layout Type", + "force": "Force Layout", + "circular": "Circular Layout", + "concentric": "Concentric Layout", + "dagre": "Dagre Layout", + "grid": "Grid Layout", + "radial": "Radial Layout", + "node_size": "Node Size", + "node_size_tooltip": "Node size (diameter), used for collision detection.", + "link_distance": "Link Distance", + "node_strength": "Node Strength", + "node_strength_tooltip": "Node force. Positive values attract nodes, and negative values repel nodes.", + "prevent_overlap": "Prevent Overlap", + "node_spacing": "Node Spacing", + "node_spacing_tooltip": "Takes effect when preventOverlap is enabled. Minimum spacing between node edges.", + "start_radius": "Start Radius", + "start_radius_tooltip": "Start radius of the spiral layout", + "end_radius": "End Radius", + "end_radius_tooltip": "End radius of the spiral layout", + "divisions": "Divisions", + "divisions_tooltip": "Number of divisions on the circle", + "angle_ratio": "Angle Ratio", + "angle_ratio_tooltip": "How many 2 * PI intervals exist from the first node to the last node", + "ordering": "Ordering", + "ordering_tooltip": "Ordering basis for nodes on the circle", + "ordering_data": "Data", + "ordering_topology": "Topology", + "ordering_degree": "Degree", + "clockwise": "Clockwise", + "sweep": "Sweep", + "sweep_tooltip": "Sweep between the first node and the last node.", + "start_angle": "Start Angle", + "start_angle_tooltip": "Start angle of the layout", + "equidistant": "Equal Ring Distance", + "rankdir": "Layout Direction", + "rankdir_tooltip": "Layout direction", + "align": "Align", + "align_tooltip": "Node alignment", + "ranksep": "Rank Separation", + "ranksep_tooltip": "Layer spacing in px. For TB or BT, it is vertical layer spacing. For LR or RL, it is horizontal layer spacing.", + "nodesep": "Node Separation", + "nodesep_tooltip": "Node spacing in px. For TB or BT, it is horizontal node spacing. For LR or RL, it is vertical node spacing.", + "top_bottom": "Top to Bottom", + "bottom_top": "Bottom to Top", + "left_right": "Left to Right", + "right_left": "Right to Left", + "align_center": "Center", + "align_ul": "Upper Left", + "align_ur": "Upper Right", + "align_dl": "Lower Left", + "align_dr": "Lower Right", + "rows": "Grid Rows", + "rows_tooltip": "Number of grid rows. When undefined, the algorithm calculates it from node count, layout space, and cols if specified.", + "cols": "Grid Columns", + "cols_tooltip": "Number of grid columns. When undefined, the algorithm calculates it from node count, layout space, and rows if specified.", + "unit_radius": "Level Distance", + "unit_radius_tooltip": "Distance between each ring and the previous ring. By default, it fills the canvas based on graph size.", + "link_distance_short": "Edge Length", + "focus_node": "Center Node", + "focus_node_tooltip": "Radiation center. Defaults to the first node in data. A node id or node object can be passed.", + "radial_node_spacing_tooltip": "Takes effect when preventOverlap is true. Minimum spacing between node edges.", + "radial_prevent_overlap_tooltip": "Whether to prevent overlap. Use with nodeSize. Collision detection works only when nodeSize matches the current graph node size.", + "strict_radial": "Strict Radial", + "strict_radial_tooltip": "Whether the layout must be a strict radial layout" + }, + "statistics_panel": { + "label_statistics": "Label Statistics", + "graph_statistics": "Graph Statistics", + "type_count": "Vertex and Edge Counts", + "type_count_desc": "Counts of all vertex and edge types in the current canvas.", + "degree_top10": "Top 10 Node Degrees", + "degree_top10_desc": "The ten nodes with the highest one-hop edge counts in the current canvas.", + "zero_degree_nodes": "Isolated Nodes", + "zero_degree_nodes_desc": "Isolated nodes are nodes that are not connected to other nodes in the canvas.", + "zero_degree_edges": "Isolated Edges", + "zero_degree_edges_desc": "Edges whose endpoint nodes only have one-hop associations.", + "incidence_nodes": "Adjacent Nodes", + "incidence_nodes_desc": "Select a node in the graph to show the number of its adjacent nodes.", + "select_node": "Select a node", + "highlight": "Highlight", + "hide": "Hide" + }, + "dynamic_add": { + "add_success": "Added successfully", + "save_success": "Saved successfully", + "save_failed": "Save failed", + "get_edge_type_failed": "Failed to get edge types", + "get_edge_property_failed": "Failed to get edge properties", + "get_vertex_property_failed": "Failed to get vertex properties", + "nullable_property": "Nullable Properties", + "non_nullable_property": "Required Properties", + "property": "Property", + "property_value": "Property Value", + "property_placeholder": "Enter property value", + "add_vertex": "Add Vertex", + "add_in_edge": "Add In Edge", + "add_out_edge": "Add Out Edge", + "add": "Add", + "cancel": "Cancel", + "vertex_type": "Vertex Type", + "edge_type": "Edge Type", + "source": "Source", + "target": "Target", + "id_strategy": "ID Strategy", + "primary_key": "Primary Key", + "id_value": "ID Value", + "id_placeholder": "Enter ID value", + "select_vertex_type": "Select vertex type", + "select_edge_type": "Select edge type", + "select_source": "Select source ID", + "select_target": "Select target ID", + "customize_string": "Custom String", + "primary_key_id": "Primary Key ID", + "customize_number": "Custom Number", + "automatic": "Automatic", + "required": "This field is required" + }, + "edit_element": { + "save_success": "Saved successfully", + "save_failed": "Save failed", + "get_edge_property_failed": "Failed to get edge properties", + "get_vertex_property_failed": "Failed to get vertex properties", + "type": "Type", + "edge_relation": "Edge Relation", + "parent_edge": "Parent Edge", + "child_edge": "Child Edge", + "id": "ID", + "source": "Source", + "target": "Target", + "primary_key": "Primary Key", + "nullable_property": "Nullable Properties", + "non_nullable_property": "Required Properties", + "property": "Property", + "property_value": "Property Value", + "property_placeholder": "Enter property value", + "edit_details": "Edit Details", + "data_details": "Data Details", + "save": "Save", + "edit": "Edit", + "close": "Close", + "edge_type": "Edge Type", + "edge_id": "Edge ID", + "vertex_type": "Vertex Type", + "vertex_id": "Vertex ID", + "required": "This field is required", + "edge": "Edge", + "vertex": "Vertex" + } + }, + "logs": { + "execute_tab": "Execution Records", + "favorite_tab": "Favorite Queries", + "favorite_success": "Favorited successfully", + "favorite_failed": "Failed to favorite", + "delete_success": "Deleted successfully", + "delete_failed": "Delete failed", + "edit_success": "Updated successfully", + "edit_failed": "Update failed", + "favorite_statement": "Favorite Statement", + "favorite_name_placeholder": "Enter favorite name", + "edit_name": "Edit Name", + "confirm_delete": "Confirm Delete", + "delete_favorite_confirm": "Delete this favorite statement?", + "search_placeholder": "Search favorite names or statements", + "column": { + "time": "Time", + "type": "Execution Type", + "content": "Execution Content", + "status": "Status", + "duration": "Duration", + "action": "Actions", + "name": "Name", + "favorite_statement": "Favorite Statement" + }, + "type": { + "GREMLIN": "GREMLIN Query", + "GREMLIN_ASYNC": "GREMLIN Task", + "ALGORITHM": "Algorithm Task", + "CYPHER": "CYPHER Query" + }, + "status": { + "SUCCESS": "Success", + "ASYNC_TASK_SUCCESS": "Submitted", + "ASYNC_TASK_RUNNING": "Submitting", + "RUNNING": "Running", + "FAILED": "Failed", + "ASYNC_TASK_FAILED": "Submit Failed" + }, + "failure_reason": { + "GREMLIN_EXECUTION_FAILED": "Query failed. Review the statement and try again." + }, + "action": { + "favorite": "Favorite", + "load_statement": "Load Statement", + "edit_name": "Edit Name", + "save": "Save" + } + }, + "algorithm": { + "name": "Graph Algorithm", + "placeholder": "Algorithm Query", + "run": "Run", + "task_submit_success": "{{name}} task submitted successfully", + "common": { + "instance_num": "Instance Count", + "input_limit_edges_per_vertex": "Max Outgoing Edges", + "max_iter_step": "Max Iterations", + "worker_num": "Worker Threads", + "sample_rate": "Edge sample rate. This algorithm grows exponentially and requires high compute capacity, so choose a practical sample rate for an approximate result.", + "weight_property": "Weight Property Name", + "property_filter": "Vertex/Edge Property Filter", + "min_ring_length": "Minimum ring length to output", + "max_ring_length": "Maximum ring length to output", + "max_step": "Max Iteration Steps", + "request_memory": "Minimum Memory Required per Compute Node", + "JVM_memory": "JVM memory size, 32 GB by default", + "source": "Source Vertex ID", + "query_tooltip": "OLAP algorithms can be used only after the selected graph data is loaded." + }, + "form": { + "vertices_required": "Specify ids, or specify both label and properties", + "vertices_ids_tooltip": "Provide source or target vertices by vertex ID list. If ids is empty, label and properties are used together to query vertices.", + "vertices_label_tooltip": "Vertex label. label and properties take effect only when ids is empty.", + "vertices_properties_tooltip": "Property map. Keys are property names and values are property values defined by schema. Values can be lists; a property matches when its value is in the list.", + "direction_options": { + "out": "Outgoing", + "in": "Incoming", + "both": "Both" + }, + "edge_steps": { + "label": "Edge label", + "properties": "Filter edges by property values" + }, + "vertex_steps": { + "label": "Vertex label", + "properties": "Filter vertices by property values" + }, + "step": { + "steps": "Step list from the source vertex", + "direction": "Traversal direction from the source vertex", + "labels": "Edge label list", + "edge_properties": "Property map used to filter edges. Keys are property names and values are property values defined by schema. Values can be lists; a property matches when its value is in the list.", + "max_depth": "Number of steps", + "max_times": "Repeat count for the current step. N means this step can be repeated 1 to N times from the source vertex.", + "max_degree": "Maximum adjacent edges traversed from a single vertex during the query", + "max_degree_compatible": "Maximum adjacent edges traversed from a single vertex. Before 0.12, step only supported degree; since 0.12 it uses max_degree and remains backward compatible with degree.", + "skip_degree": "Minimum edge count for skipping super vertices. When a vertex has more adjacent edges than skip_degree, the vertex is skipped. Optional; when enabled, skip_degree must be >= max_degree. Default 0 disables skipping. Enabling it may add traversal overhead.", + "edge_steps": "Edge step list", + "vertex_steps": "Vertex step list" + } + }, + "validation": { + "integer": "Enter an integer", + "range_or_minus_one": "Enter -1 or an integer greater than 0", + "max_depth_range": "Enter an integer in the range (0, 5000]", + "max_limit_range": "Enter an integer in the range (0, 800000)", + "non_negative_integer": "Enter an integer greater than or equal to 0", + "positive_integer": "Enter an integer greater than 0", + "skip_degree_range": "Enter an integer in the range [0, 10000000]", + "group_property_range": "Enter an integer greater than 2", + "alpha_range": "Enter a number in the range (0, 1]", + "top_range": "Enter a number in the range (0, 1000)", + "open_unit_range": "Enter a number in the range (0, 1)", + "depth_2_50": "Enter an integer in the range [2, 50]", + "properties_format": "Enter one key=value pair per line", + "string_value_quote": "Wrap string values in single quotes", + "open_unit_value": "Enter a value in the range (0, 1)", + "closed_unit_value": "Enter a value in the range [0, 1]", + "integer_1_2000": "Enter an integer from 1 to 2000", + "value_1_100000": "Enter a value from 1 to 100000", + "integer_0_100000": "Enter an integer in the range [0, 100000]", + "integer_1_10000": "Enter an integer from 1 to 10000" + }, + "result": { + "similarity_value": "Similarity Value", + "rank_score": "Rank Score:", + "category": "Category {{index}}", + "no_neighbor_at_degree": "No {{index}}-hop neighbors in this scenario" + }, + "mode": { + "OLTP": "OLTP Algorithm", + "OLAP": "OLAP Algorithm" + }, + "capacity_item": { + "tooltip": "Maximum number of vertices visited during traversal" + }, + "direction_item": { + "tooltip": "Traversal direction from the vertex" + }, + "label_item": { + "tooltip": "Edge type. Leave empty to include all edge labels." + }, + "max_degree_item": { + "tooltip": "Maximum adjacent edges traversed from a single vertex during the query" + }, + "max_depth_item": { + "tooltip": "Number of steps" + }, + "nearest_item": { + "tooltip": "When nearest is true, the shortest path length from the source vertex to the result vertex is depth and no shorter path exists. When nearest is false, there is a path with length depth from the source vertex to the result vertex, but it may not be shortest and may contain cycles.", + "placeholder": "Shortest path length" + }, + "olap": { + "betweenness_centrality": { + "desc": "Betweenness Centrality estimates how likely a node is to act as a bridge on paths between other nodes. Higher values indicate stronger bridge behavior, such as common followed accounts in a social network.", + "sample_rate": "Edge Sample Rate", + "sample_rate_long": "Edge sample rate. This algorithm grows exponentially and requires high compute capacity, so choose a practical sample rate for an approximate result.", + "use_endpoint": "Include the last vertex in computation" + }, + "closeness_centrality": { + "desc": "Computes the reciprocal of the shortest distance from a node to all other reachable nodes, accumulates the values, and normalizes the result. It is used to calculate closeness centrality for each node and supports directed and undirected graphs.", + "weight_property": "Weight Property Name", + "sample_rate": "Edge Sample Rate", + "wf_improved": "Use the Wasserman and Faust closeness centrality formula" + }, + "cluster_coefficient": { + "desc": "Clustering coefficient computes the local clustering coefficient for each vertex. Global clustering coefficient is not available yet.", + "minimum_edges_use_superedge_cache": "Use memory to reduce message volume. If memory is insufficient, change 100 to 1000, but the clustering coefficient job may not complete." + }, + "degree_centrality": { + "desc": "Computes degree centrality for each node and supports directed and undirected graphs.", + "direction": "Direction: in/out/both for incoming, outgoing, or both directions" + }, + "filtered_rings_detection": { + "desc": "Filtered Rings Detection detects rings in the graph. Each ring path is recorded by the vertex with the smallest ID in the ring. Vertex and edge property filters can be specified to selectively propagate paths." + }, + "filter_subgraph_matching": { + "desc": "Filter Subgraph Matching accepts a query graph with property filters and matches all subgraphs that are isomorphic to the query graph.", + "query_graph_config": "Query graph configuration as a JSON array string" + }, + "k_core": { + "desc": "K-Core marks all vertices whose degree is K.", + "k": "K value for the K-Core algorithm. Optional, with a default value.", + "degree_k": "Minimum degree threshold" + }, + "label_propagation_algorithm": { + "desc": "Label Propagation Algorithm is a graph clustering algorithm commonly used in social networks." + }, + "links": { + "desc": "Links tracing starts from a set of source vertices, propagates according to specified rules, stops when the end condition is met, and records the resulting paths.", + "analyze_config": "Link propagation condition configuration" + }, + "louvain": { + "desc": "Louvain is a modularity-based community detection algorithm. Because of its algorithm characteristics, it runs with only one worker instance." + }, + "computer_item": { + "computer_cpu": "Master Max CPU", + "worker_cpu": "Worker Max CPU", + "master_request_memory": "Master Minimum Memory. Allocation fails if the minimum is not met.", + "worker_request_memory": "Worker Minimum Memory. Allocation fails if the minimum is not met.", + "master_memory": "Master Max Memory. The task is terminated by Kubernetes if this limit is exceeded.", + "worker_memory": "Worker Max Memory. The task is terminated by Kubernetes if this limit is exceeded." + }, + "item": { + "PAGE_RANK": "PageRank", + "WEAKLY_CONNECTED_COMPONENT": "Weakly Connected Component", + "DEGREE_CENTRALIT": "Degree Centrality", + "CLOSENESS_CENTRALITY": "Closeness Centrality", + "TRIANGLE_COUNT": "Triangle Count", + "K_NEIGHBOR": "K-neighbor (GET, Basic)", + "K_OUT": "K-out API (GET, Basic)", + "SAME_NEIGHBORS": "Same Neighbors", + "RINGS": "Rings", + "SHORTEST_PATH": "Shortest Path", + "ALLPATHS": "Find All Paths (POST, Advanced)", + "JACCARD_SIMILARITY": "Jaccard Similarity (GET)", + "CROSSPOINTS": "Crosspoints", + "RINGS_DETECTION": "Rings Detection", + "FILTERED_RINGS_DETECTION": "Filtered Rings Detection", + "LINKS": "Links", + "CLUSTER_COEFFICIENT": "Cluster Coefficient", + "BETWEENNESS_CENTRALITY": "Betweenness Centrality", + "LABEL_PROPAGATION_ALGORITHM": "Label Propagation Algorithm", + "LOUVAIN": "Louvain", + "FILTER_SUBGRAPH_MATCHING": "Filter SubGraph Matching", + "K_CORE": "K-Core", + "PERSONAL_PAGE_RANK": "PersonalPageRank", + "KOUT_POST": "K-out API (POST, Advanced)", + "KNEIGHBOR_POST": "K-neighbor API (POST, Advanced)", + "JACCARD_SIMILARITY_POST": "Jaccard Similarity (POST)", + "RANK_API": "rank API", + "NEIGHBOR_RANK_API": "Neighbor Rank API", + "FINDSHORTESTPATH": "Find Shortest Path", + "FINDSHORTESTPATHWITHWEIGHT": "Find Weighted Shortest Path", + "SINGLESOURCESHORTESTPATH": "Find Shortest Paths from One Vertex", + "MULTINODESSHORTESTPATH": "Find Shortest Paths for Specified Vertices", + "CUSTOMIZEDPATHS": "Customized Path Query", + "TEMPLATEPATHS": "Template Path Query", + "CUSTOMIZED_CROSSPOINTS": "Customized Crosspoints", + "RAYS": "Rays", + "PATHS": "Find All Paths (GET, Basic)", + "FUSIFORM_SIMILARITY": "Fusiform Similarity", + "ADAMIC_ADAR": "Adamic Adar", + "RESOURCE_ALLOCATION": "Resource Allocation", + "SAME_NEIGHBORS_BATCH": "Same Neighbors Batch", + "EGONET": "Egonet", + "SSSP": "SSSP (Single Source Shortest Path)" + }, + "legacy_item_alias": { + "KOUT_POST": "K-out API(POST, Advanced)" + }, + "page_rank": { + "desc": "PageRank ranks web pages or graph nodes by computing relationships through links, reflecting node relevance and importance.", + "alpha": "Weight coefficient, also known as damping factor", + "l1": "Convergence precision. It is the upper bound of the accumulated absolute changes between two iterations. The algorithm stops when the value is below this threshold.", + "damping": "Damping factor, the percentage propagated to the next node", + "diff": "Convergence precision. It is the upper bound of the accumulated absolute changes between two iterations. The algorithm stops when the value is below this threshold." + }, + "personal_page_bank": { + "desc": "PersonalPageRank is a personalized recommendation algorithm that computes relevance and importance from links between pages or graph nodes.", + "source": "Source Vertex", + "alpha": "Weight coefficient, also known as damping factor", + "l1": "Convergence Precision", + "use_id_fixlength": "When true, the system uses auto-increment ID calculation", + "use_id_fixlength_query": "Whether to use auto-increment ID calculation" + }, + "ring_detection": { + "desc": "Rings Detection detects rings in the graph. Each ring path is recorded by the vertex with the smallest ID in the ring." + }, + "SSSPVermeer": { + "desc": "Single Source Shortest Path computes the shortest distance from one vertex to all other vertices." + }, + "triangle_count": { + "desc": "Triangle Count computes the number of triangles that each vertex participates in.", + "limit_edges_in_one_vertex": "Max Outgoing Edges", + "minimum_edges_use_superedge_cache": "Use memory to reduce message volume. If memory is insufficient, change 100 to 1000, but the triangle count job may not complete." + }, + "weakly_connected_component": { + "desc": "Weakly Connected Component computes all connected subgraphs in an undirected graph and outputs the weakly connected component ID for each vertex." + } + }, + "oltp": { + "common": { + "source_vertex_id": "Source vertex ID", + "target_vertex_id": "Target vertex ID", + "vertex_id": "Vertex ID", + "other_vertex_id": "Other vertex ID", + "direction_source_target": "Direction from the source vertex to the target vertex. The reverse direction is target to source. BOTH ignores direction.", + "limit_crosspoints": "Maximum number of returned crosspoints", + "max_steps": "Maximum number of steps", + "weight_property": "Edge weight property. The property type must be numeric. If it is empty or an edge does not have the property, the weight is 1.0.", + "alpha": "Probability of walking outward from a vertex in each iteration, similar to alpha in PageRank.", + "iterations": "Number of iterations", + "limit_vertices": "Maximum number of returned vertices", + "source_target_path": "Path traversed from the source vertex to the target vertex" + }, + "template_paths": { + "desc": "Find all paths that match a set of source vertices, edge rules including direction, labels, and property filters, and max depth.", + "sources": "Source Vertices", + "targets": "Target Vertices", + "with_ring": "true includes rings; false excludes rings", + "capacity": "Maximum number of vertices visited during traversal", + "limit": "Maximum number of returned paths" + }, + "kout_get": { + "desc": "Find vertices reachable from the source vertex in exactly depth steps, using optional direction and edge label filters." + }, + "kneighbor_get": { + "desc": "Find all vertices reachable within depth steps from the source vertex, including the source vertex, using optional direction and edge label filters." + }, + "kout_post": { + "desc": "Find vertices reachable from the source vertex in exactly depth steps using step rules.", + "source": "Source Vertex ID", + "capacity": "Maximum number of vertices visited during traversal", + "limit": "Maximum number of returned vertices", + "algorithm": "Traversal strategy. deep_first usually performs better, but when nearest is true it may include non-nearest vertices, especially on large datasets.", + "algorithm_placeholder": "Select traversal strategy", + "breadth_first": "Breadth First", + "deep_first": "Depth First" + }, + "same_neighbors": { + "desc": "Find common neighbors of two vertices.", + "limit": "Maximum number of returned common neighbors" + }, + "rings": { + "desc": "Find reachable rings from the source vertex using optional direction, edge label, and max depth filters.", + "source_in_ring": "Whether the ring must contain the source vertex" + }, + "shortest_path": { + "desc": "Find one shortest path using source vertex, target vertex, direction, optional edge label, and max depth." + }, + "jaccard_similarity_get": { + "desc": "Compute the Jaccard similarity of two vertices: the intersection of their neighbors divided by the union of their neighbors." + }, + "crosspoints": { + "desc": "Find crosspoints using source vertex, target vertex, direction, optional edge label, and max depth." + }, + "find_shortest_path": { + "desc": "Find all shortest paths between two vertices using source vertex, target vertex, direction, optional edge label, and max depth." + }, + "find_shortest_path_with_weight": { + "desc": "Find one weighted shortest path using source vertex, target vertex, direction, optional edge label, and max depth." + }, + "paths": { + "desc": "Find all paths using source vertex, target vertex, direction, optional edge label, and max depth.", + "limit": "Number of target vertices to query and number of returned shortest paths" + }, + "rays": { + "desc": "Find paths that radiate from the source vertex to boundary vertices using optional direction, edge label, and max depth filters." + }, + "rank_api": { + "desc": "Recommend other vertices with similar or related connections based on a source vertex's existing outgoing edges.", + "label": "Edge label starting from the source vertex. It must connect two different vertex categories.", + "max_diff": "Convergence precision for early stop", + "sorted": "Whether to sort returned results by rank", + "with_label": "Choose which result categories to keep: SAME_LABEL, OTHER_LABEL, or BOTH_LABEL.", + "same_label": "Keep only vertices with the same category as the source vertex", + "other_label": "Keep only vertices with a different category from the source vertex, usually the other side of a bipartite graph", + "both_label": "Keep vertices from both the same and opposite categories as the source vertex" + }, + "neighbor_rank_api": { + "desc": "Find the top N vertices most related to the source vertex at each layer in a general graph structure, along with their relevance scores.", + "top": "Keep only the top N weighted results at each layer" + }, + "jaccard_similarity_post": { + "desc": "Find the top N vertices with the highest Jaccard similarity to the specified vertex.", + "top": "Return the top vertices with the highest Jaccard similarity to a source vertex" + }, + "kneighbor_post": { + "desc": "Find all vertices reachable within depth steps from the source vertex using step rules, including direction, edge labels, and property filters." + }, + "single_source_shortest_path": { + "desc": "Starting from one vertex, find shortest paths from that vertex to other vertices in the graph, optionally with edge weights." + }, + "multi_nodes_shortest_path": { + "desc": "Find shortest paths between every pair in a specified vertex set.", + "vertices": "Source Vertices" + }, + "all_paths": { + "desc": "Find all paths using source vertices, target vertices, step rules, and max depth." + }, + "fusiform_similarity": { + "desc": "Find fusiform-similar vertices for a set of source vertices under the specified conditions.", + "min_neighbors": "Minimum neighbor count. If a source vertex has fewer neighbors than this threshold, it is not considered to have fusiform-similar vertices.", + "alpha": "Similarity threshold: the ratio of common neighbors shared by the source vertex and the fusiform-similar vertex to all neighbors of the source vertex.", + "min_similars": "Minimum number of fusiform-similar vertices. The source vertex and its similar vertices are returned only when the count is greater than or equal to this value.", + "top": "Return the top N fusiform-similar vertices with the highest similarity for each source vertex. 0 means all.", + "group_property": "Used with min_groups. When the source vertex and its fusiform-similar vertices have at least min_groups different values for this property, the source vertex and its similar vertices are returned. Leave empty to skip this property filter.", + "min_groups": "Used with group_property; meaningful only when group_property is set.", + "limit": "Maximum number of returned result groups. One source vertex and its fusiform-similar vertices count as one result.", + "with_intermediary": "Whether to return intermediate vertices shared by the source vertex and its fusiform-similar vertices." + }, + "resource_allocation": { + "desc": "Resource Allocation is commonly used in social networks to estimate how closely two vertices are related based on their common neighbors." + }, + "same_neighbors_batch": { + "desc": "Batch query common neighbors for pairs of vertices.", + "vertex_list": "List of vertex ID pairs, for example: [[\"0000000001\",\"0000376440\"],[\"0000000001\",\"0001822679\"]]" + }, + "egonet": { + "desc": "Find K-hop neighbors of multiple source vertices, with optional filters for different vertex and edge labels. Results include the source vertices.", + "sources": "Source vertex ID list. Multiple vertices are supported." + }, + "customized_paths": { + "desc": "Find all paths that match a set of source vertices, edge rules including direction, labels, and property filters, and max depth.", + "sort_by": "Sort by path weight: NONE means no sorting, INCR sorts by ascending path weight, and DECR sorts by descending path weight.", + "sort_none": "No sorting", + "sort_incr": "Sort by path weight ascending", + "sort_decr": "Sort by path weight descending", + "weight_by": "Use the specified property to calculate edge weight. It is effective when sort_by is not NONE and is mutually exclusive with default_weight.", + "default_weight": "Default edge weight used when an edge does not have the weight property. It is effective when sort_by is not NONE and is mutually exclusive with weight_by.", + "sample": "Sample matching edges for the current step. -1 means no sampling." + }, + "customized_crosspoints": { + "desc": "Find the intersection of path endpoints that match multiple path rules, including direction, edge labels, property filters, and max depth.", + "path_patterns": "Path rule list traversed from the source vertex", + "limit": "Maximum number of returned paths" + }, + "adamic_adar": { + "desc": "Adamic-Adar is commonly used in social networks to estimate how closely two vertices are related based on the density of their common neighbors.", + "vertex": "Source Vertex", + "other": "Target Vertex", + "direction": "Direction from the source vertex to the target vertex. The reverse direction is target to source. BOTH ignores direction.", + "label": "Leave empty to include all edge labels", + "max_degree": "Maximum adjacent edges traversed from a single vertex during the query", + "select_direction": "Traversal direction from the vertex" + } + } + }, + "async_task": { + "name": "Task Management", + "search_placeholder": "Enter a task ID or name", + "get_failed": "Failed to get async tasks", + "delete_failed": "Delete failed", + "abort_failed": "Abort failed", + "delete_confirm_title": "Delete Task", + "delete_confirm_content": "Delete this task? This cannot be undone.", + "batch_delete_title": "Batch Delete", + "batch_delete_content": "Delete the selected tasks? This cannot be undone.", + "selected_count": "{{count}} selected", + "batch_delete": "Batch Delete", + "column": { + "task_id": "Task ID", + "task_name": "Task Name", + "task_type": "Task Type", + "create_time": "Created At", + "duration": "Duration", + "status": "Status", + "action": "Action" + }, + "type": { + "all": "All", + "gremlin": "Gremlin Task", + "algorithm": "Algorithm Task", + "remove_schema": "Remove Metadata", + "create_index": "Create Index", + "rebuild_index": "Rebuild Index", + "cypher": "Cypher Task", + "vermeer_load": "Vermeer Graph Load Task", + "vermeer_compute": "Vermeer Graph Compute Task" + }, + "status": { + "all": "All", + "unknown": "Unknown", + "new": "Initializing", + "scheduling": "Scheduling", + "scheduled": "Scheduled", + "queued": "Queued", + "running": "Running", + "restoring": "Restoring", + "success": "Success", + "failed": "Failed", + "cancelled": "Cancelled", + "cancelling": "Cancelling", + "hanging": "Hanging", + "pending": "Pending", + "deleting": "Deleting" + }, + "action": { + "abort": "Abort", + "aborting": "Aborting", + "delete": "Delete", + "check_result": "View Result", + "check_reason": "View Reason" + } + }, + "topbar": { + "current_graph_space": "Current Graphspace:", + "current_graph": "Current Graph:", + "select": "Select", + "loaded": "Loaded", + "loading": "Loading", + "load_failed": "Load Failed", + "not_loaded": "Not Loaded", + "recent_load_time": "Last Load Time:", + "load_to_vermeer": "Load to Vermeer", + "reload_to_vermeer": "Reload to Vermeer", + "metadata_config": "Metadata", + "metadata_tooltip": "Open metadata configuration for the selected graph", + "olap_result": "Query OLAP Result:", + "get_vermeer_failed": "Failed to get Vermeer status", + "load_vermeer_failed": "Failed to load Vermeer task" + } + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/home.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/home.json new file mode 100644 index 000000000..4f89f2ed3 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/home.json @@ -0,0 +1,10 @@ +{ + "home": { + "navigation": "Navigation", + "name":"System Management" , + "my": "My Profile", + "account": "Account Management", + "resource": "Resource Management", + "role": "Role Management" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/index.js b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/index.js new file mode 100644 index 000000000..9155cab6f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/index.js @@ -0,0 +1,30 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import Home from './home.json'; +import Manage from './manage.json'; +import Analysis from './analysis.json'; +import Modules from './modules.json'; +import Pages from './pages.json'; +export { + Home, + Analysis, + Manage, + Modules, + Pages, +}; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/manage.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/manage.json new file mode 100644 index 000000000..e23360658 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/manage.json @@ -0,0 +1,8 @@ +{ + "manage": { + "name": "Data Management", + "graphspace": "Graph Management", + "source": "Data Source Management", + "task": "Data Import" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/modules.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/modules.json new file mode 100644 index 000000000..be01f95fd --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/modules.json @@ -0,0 +1,290 @@ +{ + "topbar": { + "current_graphspace": "Graph Space:", + "current_graph": "Graph:", + "graph_select_placeholder": "Please select", + "status": { + "loaded": "Loaded", + "loading": "Loading", + "error": "Load Failed", + "created": "Not Loaded" + }, + "last_load_time": "Last loaded:", + "reload_to_vermeer": "Reload to Vermeer", + "load_to_vermeer": "Load to Vermeer", + "meta_config": "Metadata Config", + "meta_config_tooltip": "Click to navigate to the metadata configuration page", + "olap_switch": "Query OLAP results:", + "olap_on": "On", + "olap_off": "Off", + "load_fail": "Failed to load Vermeer task" + }, + "execute_log": { + "col": { + "time": "Time", + "type": "Type", + "content": "Content", + "status": "Status", + "duration": "Duration", + "operation": "Actions" + }, + "type": { + "GREMLIN": "GREMLIN Query", + "GREMLIN_ASYNC": "GREMLIN Task", + "ALGORITHM": "Algorithm Task", + "CYPHER": "CYPHER Query" + }, + "status": { + "SUCCESS": "Success", + "ASYNC_TASK_SUCCESS": "Submitted", + "ASYNC_TASK_RUNNING": "Submitting", + "RUNNING": "Running", + "FAILED": "Failed", + "ASYNC_TASK_FAILED": "Submit Failed" + }, + "favorite": "Favorite", + "load_statement": "Load", + "favorite_title": "Favorite Statement", + "favorite_placeholder": "Enter favorite name" + }, + "favorite": { + "col": { + "name": "Name", + "content": "Content", + "create_time": "Saved At", + "operation": "Actions" + }, + "edit_title": "Edit Name", + "edit_placeholder": "Enter favorite name", + "load_statement": "Load", + "delete_confirm": "Confirm delete?", + "search_placeholder": "Search favorite name" + }, + "query_bar": { + "tab_execute": "Execution Log", + "tab_favorite": "Favorites", + "gremlin_placeholder": "Please enter a query statement", + "execute": "Execute", + "async_execute": "Async Execute", + "clear": "Clear" + }, + "query_result": { + "tab_graph": "Graph", + "tab_table": "Table", + "tab_json": "JSON", + "empty": "No results", + "vertex_count": "Vertices: {{count}}", + "edge_count": "Edges: {{count}}" + }, + "async_task": { + "col": { + "id": "Task ID", + "type": "Type", + "status": "Status", + "create_time": "Create Time", + "update_time": "Update Time", + "operation": "Actions" + }, + "detail": { + "title": "Task Detail", + "id": "Task ID", + "type": "Type", + "status": "Status", + "create_time": "Create Time", + "update_time": "Update Time", + "content": "Content", + "result": "Result" + }, + "cancel": "Cancel Task", + "cancel_confirm": "Confirm cancel this task?" + }, + "graph_component": { + "search": { + "placeholder": "Search vertex ID or property value", + "search_btn": "Search", + "vertex_id": "Vertex ID", + "property": "Property", + "result_count": "Found {{count}} results" + }, + "edit_element": { + "title_vertex": "Edit Vertex", + "title_edge": "Edit Edge", + "id": "ID", + "label": "Type", + "properties": "Properties", + "save": "Save", + "cancel": "Cancel", + "delete": "Delete" + }, + "export": { + "title": "Export Data", + "json": "Export JSON", + "csv": "Export CSV", + "image": "Export Image" + }, + "menu": { + "expand": "Expand", + "hide": "Hide", + "show_properties": "Show Properties", + "hide_properties": "Hide Properties", + "fix": "Pin", + "unfix": "Unpin" + }, + "status": { + "vertex_count": "Vertices: {{count}}", + "edge_count": "Edges: {{count}}" + }, + "clear": "Clear Graph", + "clear_confirm": "Confirm clear graph?", + "fit_center": "Fit Canvas", + "fix_node": "Pin Node", + "full_screen": "Fullscreen", + "zoom_in": "Zoom In", + "zoom_out": "Zoom Out", + "redo": "Redo", + "undo": "Undo", + "refresh": "Refresh", + "zero_degree": "Show Isolated Nodes", + "legend": "Legend", + "3d_mode": "3D Mode", + "2d_mode": "2D Mode" + }, + "filter": { + "title": "Filter", + "add": "Add Filter", + "vertex_filter": "Vertex Filter", + "edge_filter": "Edge Filter", + "property": "Property", + "operator": "Operator", + "value": "Value", + "apply": "Apply", + "reset": "Reset", + "and": "AND", + "or": "OR" + }, + "layout": { + "title": "Layout Config", + "type": "Layout Type", + "force": "Force", + "circular": "Circular", + "radial": "Radial", + "dagre": "Dagre", + "grid": "Grid", + "concentric": "Concentric", + "apply": "Apply", + "node_size": "Node Size", + "link_distance": "Link Distance", + "node_strength": "Node Strength", + "edge_strength": "Edge Strength", + "gravity": "Gravity", + "speed": "Speed", + "iterations": "Iterations", + "radius_ratio": "Radius Ratio", + "node_sep": "Node Spacing", + "rank_sep": "Rank Spacing", + "direction": "Direction", + "LR": "Left to Right", + "RL": "Right to Left", + "TB": "Top to Bottom", + "BT": "Bottom to Top", + "start_radius": "Start Radius", + "end_radius": "End Radius", + "sweep": "Sweep", + "equal_node_spacing": "Equal Spacing", + "min_node_spacing": "Min Spacing", + "start_angle": "Start Angle", + "clockwise": "Clockwise", + "prevent_overlap": "Prevent Overlap", + "node_spacing": "Node Spacing", + "cols": "Columns", + "rows": "Rows" + }, + "style_config": { + "title": "Style Config", + "color_picker": "Choose color", + "close_color_picker": "Close color picker", + "vertex_style": "Vertex Style", + "edge_style": "Edge Style", + "vertex_appearance": "Vertex Appearance", + "edge_appearance": "Edge Appearance", + "basic_properties": "Basic Properties", + "appearance_type": "Vertex/Edge Appearance", + "appearance_config": "Appearance Config", + "select_placeholder": "Please select", + "type": "Type", + "shape": "Shape", + "shape_circle": "Circle", + "shape_diamond": "Diamond", + "shape_triangle": "Triangle", + "shape_star": "Star", + "shape_ellipse": "Ellipse", + "border_color": "Border Color", + "border_width": "Border Width", + "fill_color": "Fill Color", + "icon_style": "Icon Style", + "icon_color": "Icon Color", + "label_size": "Label Size", + "label_color": "Label Color", + "node_opacity": "Node Opacity", + "edge_width": "Edge Width", + "edge_color": "Edge Color", + "edge_animation": "Edge Animation", + "line_solid": "Solid Line", + "line_dashed": "Dashed Line", + "color": "Color", + "size": "Size", + "label": "Label", + "icon": "Icon", + "line_type": "Line Type", + "line_width": "Line Width", + "arrow": "Arrow", + "display_field": "Display Field", + "apply": "Apply", + "reset": "Reset" + }, + "statistics": { + "title": "Graph Statistics", + "vertex_count": "Vertices", + "edge_count": "Edges", + "vertex_types": "Vertex Types", + "edge_types": "Edge Types", + "loading": "Loading..." + }, + "dynamic_add": { + "vertex": { + "title": "Add Vertex", + "id": "Vertex ID", + "type": "Vertex Type", + "properties": "Properties", + "submit": "Submit", + "cancel": "Cancel" + }, + "edge": { + "title": "Add Edge", + "type": "Edge Type", + "source": "Source", + "target": "Target", + "properties": "Properties", + "submit": "Submit", + "cancel": "Cancel" + } + }, + "navigation_module": { + "create_data": { + "title": "Create Data", + "graph_management": "Graph Management", + "meta_management": "Metadata Management", + "data_import": "Data Import" + }, + "analysis": { + "title": "Business Analysis", + "graph_query": "Graph Query", + "algorithm": "Graph Algorithm" + }, + "system": { + "title": "System Management", + "user_management": "User Management", + "role_management": "Role Management" + } + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json new file mode 100644 index 000000000..c9411a28d --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json @@ -0,0 +1,833 @@ +{ + "common": { + "add_success": "Added successfully", + "batch_delete": "Delete Selected", + "create": "Create", + "delete": "Delete", + "delete_success": "Deleted successfully", + "edit": "Edit", + "no": "No", + "operation": "Actions", + "refresh": "Refresh", + "select_at_least_one": "Please select at least one item", + "update_success": "Updated successfully", + "yes": "Yes", + "action": { + "create": "Create", + "save": "Save", + "edit": "Edit", + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm", + "add": "Add", + "search": "Search", + "reset": "Reset", + "view": "View", + "detail": "Detail", + "upload": "Upload", + "download": "Download", + "refresh": "Refresh", + "submit": "Submit", + "clear": "Clear", + "next": "Next", + "back": "Back", + "close": "Close", + "apply": "Apply", + "assign_permission": "Assign Permission", + "set_default": "Set Default", + "schema_manage": "Schema Management", + "init": "Initialize", + "clone": "Clone" + }, + "label": { + "default": "Default", + "unlimited": "Unlimited", + "unknown": "Unknown", + "warning": "Warning", + "yes": "Yes", + "no": "No", + "view_mode": "Card View", + "list_mode": "List View", + "loading": "Loading..." + }, + "verify": { + "yes": "Yes", + "no": "No" + }, + "msg": { + "success": "Operation succeeded", + "create_success": "Created successfully", + "update_success": "Updated successfully", + "delete_success": "Deleted successfully", + "set_success": "Set successfully", + "init_success": "Initialized successfully", + "upload_success": "Uploaded successfully", + "upload_fail": "Upload failed", + "enable_success": "Started successfully", + "enable_fail": "Failed to start", + "disable_success": "Paused successfully", + "disable_fail": "Failed to pause", + "delete_fail": "Deletion failed", + "operation_failed": "Operation failed. Check the server connection and retry.", + "load_failed": "Unable to load data. Check the server connection and retry.", + "select_one": "Please select at least one item" + }, + "confirm": { + "delete": "Confirm delete?", + "delete_irrecoverable": "This action cannot be undone" + }, + "form": { + "required": "This field is required", + "max_12": "Max 12 characters", + "max_48": "Max 48 characters", + "max_128": "Max 128 characters" + } + }, + "graphspace": { + "title": "Graph Space Management", + "load": { + "unavailable": "Unable to load the GraphSpace list.", + "retry": "Retry GraphSpaces" + }, + "create": "Create Graph Space", + "search_placeholder": "Search by graph space name", + "set_default_confirm": "Confirm changing the default graph space?", + "delete_confirm": "Confirm delete?", + "delete_content": "This action cannot be undone", + "col": { + "name": "Name", + "auth": "Authentication", + "description": "Description", + "graph_service": "Graph Service", + "compute_task": "Compute Task", + "storage_limit": "Max Storage Limit", + "operation": "Actions" + }, + "auth_yes": "Yes", + "auth_no": "No", + "unit": { + "cpu": "vCPU", + "memory": "GB", + "unlimited": "Unlimited" + }, + "form": { + "title_edit": "Edit Graph Space", + "title_create": "Create Graph Space", + "id": "Graph Space ID", + "name": "Display Name", + "auth": "Enable Authentication", + "max_graph": "Max Graphs", + "max_role": "Max Roles", + "graph_service": "Graph Service:", + "compute_task": "Compute Task:", + "storage": "Storage Service:", + "cpu": "CPU Resources:", + "memory": "Memory Resources:", + "k8s_namespace": "K8s Namespace", + "operator_image": "Operator Image", + "algorithm_image": "Algorithm Image", + "storage_limit": "Max Storage Limit:", + "admin": "Space Admin", + "admin_placeholder": "Select space admin", + "description": "Description", + "description_placeholder": "Graph space description (optional)", + "cpu_placeholder": "vCPU", + "memory_placeholder": "GB", + "id_placeholder": "Start with a letter, lowercase letters/digits/underscore, max 48 chars", + "name_placeholder": "Letters, digits, underscore, max 12 chars", + "k8s_placeholder": "Start with a letter, lowercase letters/digits/hyphen, max 48 chars", + "image_format_error": "Please enter a valid image address (e.g. example.com/org_1/xx_img:1.0.0)", + "id_rule": "Start with a letter, lowercase letters/digits/underscore, max 48 chars", + "k8s_rule": "Start with a letter, lowercase letters/digits/hyphen, max 48 chars" + }, + "card": { + "enter": "Enter Graph Space", + "vertex": "Vertices", + "edge": "Edges", + "daily_update": "Updated daily", + "last_update": "Last updated: {{date}}", + "graph_id": "Graph ID: {{name}}", + "auth_label": "Auth: ", + "max_graph": "Max Graphs: ", + "cpu": "CPU: {{val}}", + "memory": "Memory: {{val}}", + "storage": "Max Storage: ", + "used": "Used: ", + "set_default": "Default Space", + "created": "created" + } + }, + "graph": { + "title": "Graph Management", + "create": "Create Graph", + "search_placeholder": "Search by graph name", + "unavailable": "Graphs are unavailable. Check the server connection and retry.", + "set_default_confirm": "Confirm changing the default graph?", + "set_default": { + "setting": "Setting default graph...", + "success": "Default graph updated" + }, + "delete_confirm": { + "title": "Delete this graph?", + "irreversible": "This operation cannot be undone.", + "deleting": "Deleting graph...", + "success": "Graph deleted", + "failed": "Failed to delete graph" + }, + "clear_confirm": { + "title": "Confirm graph clear", + "graphspace": "GraphSpace: {{graphspace}}", + "graph": "Graph: {{graph}}", + "scope_data": "Deletion scope: graph data. Back up schema before clearing.", + "irreversible": "This operation is irreversible.", + "input_label": "Enter the graph name to confirm", + "input_placeholder": "Graph name", + "confirm": "Clear graph", + "cancel": "Cancel", + "request_failed": "Clear failed" + }, + "col": { + "name": "Graph Name", + "auth": "Authentication", + "description": "Description", + "creator": "Creator", + "create_time": "Created At", + "operation": "Actions" + }, + "menu": { + "enter_analysis": "Open Graph Studio", + "meta_config": "Metadata Config", + "clear_data": "Clear Data", + "set_default": "Set Default", + "view_schema": "View Schema", + "clone": "Clone Graph" + }, + "schema_view": { + "export": "Export Groovy Schema", + "load_failed": "Schema could not be loaded. Check the Server connection and retry.", + "retry": "Retry schema", + "export_failed": "Schema export failed. Retry when the Server is available." + }, + "form": { + "title_create": "Create Graph", + "title_edit": "Edit Graph", + "name": "Graph ID", + "name_placeholder": "Lowercase letters, digits, underscore, max 48 chars", + "nickname": "Display Name", + "nickname_placeholder": "Letters, digits, underscore, max 48 chars", + "schema_placeholder": "Select a schema", + "schema_load_failed": "Schema templates could not be loaded.", + "schema_retry": "Retry templates", + "schema_optional_hint": "Optional. You can create an empty graph without a template.", + "create_success": "Graph created", + "update_success": "Graph updated", + "auth": "Enable Authentication", + "description": "Description", + "description_placeholder": "Graph description (optional)" + }, + "clone": { + "unavailable": "Clone is unavailable because the connected Server does not expose a compatible clone API.", + "title": "Clone Graph", + "success": "Graph cloned", + "name": "Cloned Graph ID", + "name_duplicate": "Enter an ID different from the source graph", + "nickname_label": "Cloned Graph Name", + "nickname": "{{nickname}} Copy", + "nickname_duplicate": "Enter a name different from the source graph", + "graphspace": "Destination Graph Space", + "content": "Clone Content", + "schema": "Schema only", + "schema_data": "Schema and data", + "required_disk": "Required Disk Space", + "estimated_time": "Estimated Clone Time" + }, + "detail": { + "title": "Detail", + "name": "Graph Name", + "nickname": "Display Name", + "auth": "Authentication", + "graphspace": "Graph Space", + "creator": "Creator", + "create_time": "Created At", + "last_update": "Last Updated: ", + "update_data": "Update Data", + "update_success": "Update requested. Refresh the page in a moment.", + "statistics_unavailable": "Statistics are unavailable. Retry later.", + "unavailable": "Graph details are unavailable. Check the server and retry.", + "vertex_total": "Total vertices", + "edge_total": "Total edges", + "vertex_type": "Vertex type", + "edge_type": "Edge type", + "count": "Count" + }, + "card": { + "more_actions": "More actions for {{graph}}", + "storage": "Storage", + "detail_tooltip": "View the current vertex and edge counts for this graph" + } + }, + "account": { + "title": "Account Management", + "load": { + "unavailable": "Unable to load the account list.", + "retry": "Retry accounts" + }, + "create": "Create Account", + "delete_confirm": "Confirm deleting account {{name}}?", + "col": { + "id": "Account ID", + "name": "Account Name", + "remark": "Remark", + "resource": "Resource Permission", + "create_time": "Created At", + "is_superadmin": "Super Admin", + "permission": "Admin Permission" + }, + "form": { + "title_detail": "View Account", + "title_edit": "Edit Account", + "title_auth": "Assign Permission", + "title_create": "Create Account", + "id": "Account ID", + "id_placeholder": "Login username", + "name": "Display Name", + "name_placeholder": "Can be changed after creation", + "is_superadmin": "Super Admin", + "remark": "Remark", + "remark_placeholder": "Enter account remark", + "default_password": "Default Password", + "default_password_placeholder": "123456 (changeable in profile)", + "permission": "Admin Permission" + } + }, + "my": { + "title": "My Profile", + "edit_profile_title": "Edit Profile", + "load": { + "unavailable": "Unable to load your profile.", + "retry": "Retry profile" + }, + "col": { + "id": "Account ID", + "name": "Account Name", + "is_superadmin": "Super Admin", + "remark": "Remark", + "permission": "Admin Permission", + "permission_roles": "Permissions and Roles", + "create_time": "Created At" + }, + "edit": { + "title": "Change Password", + "old_password": "Old Password", + "new_password": "New Password", + "confirm_password": "Confirm Password", + "old_password_placeholder": "Enter old password", + "new_password_placeholder": "Enter new password", + "confirm_password_placeholder": "Enter new password again", + "password_mismatch": "Passwords do not match", + "account_name_rule": "Account name must be 1-16 characters and cannot start or end with an underscore" + } + }, + "role": { + "title": "Role Management", + "create": "Create Role", + "delete_confirm": "Confirm deleting role {{name}}?", + "col": { + "name": "Role Name", + "description": "Description", + "operation": "Actions" + }, + "form": { + "title_create": "Create Role", + "title_edit": "Edit Role", + "name": "Role Name", + "description": "Description", + "permissions": "Permissions" + }, + "auth": { + "title": "Assign Permission", + "graphspace": "Graph Space", + "graph": "Graph", + "permission": "Permission" + } + }, + "resource": { + "title": "Resource Management", + "create": "Create Resource", + "col": { + "name": "Resource Name", + "type": "Type", + "graphspace": "Graph Space", + "graph": "Graph", + "operation": "Actions" + }, + "form": { + "title_create": "Create Resource", + "title_edit": "Edit Resource", + "name": "Resource Name", + "type": "Resource Type", + "graphspace": "Graph Space", + "graph": "Graph" + } + }, + "task": { + "title": "Data Import", + "search_placeholder": "Search by task name", + "pause": "Pause", + "execute": "Run", + "action": { + "detail": "View execution history", + "config": "View task configuration", + "edit": "Edit task", + "pause": "Pause task", + "run": "Run task", + "delete": "Delete task" + }, + "edit_title_create": "Create Task", + "edit_title_edit": "Edit Task", + "create": "Create Task", + "source": { + "file": "Local Upload", + "hdfs": "HDFS", + "kafka": "Kafka", + "jdbc": "JDBC" + }, + "sync": { + "once": "Run Once", + "realtime": "Realtime", + "cron": "Scheduled" + }, + "col": { + "name": "Task Name", + "source_type": "Source Type", + "target_space": "Target Space", + "target_graph": "Target Graph", + "sync_type": "Sync Type", + "status": "Status", + "create_time": "Created At", + "operation": "Actions" + }, + "statistic": { + "total": "Total", + "running": "Running", + "success": "Completed", + "failed": "Failed", + "realtime": "Realtime Tasks", + "offline": "Offline Tasks", + "pending": "Pending", + "executing": "Running", + "max_concurrency": "Max Concurrency", + "once": "One-time Tasks", + "cron": "Scheduled Tasks", + "realtime_task": "Realtime Tasks" + }, + "detail": { + "title": "Task Detail", + "success": "Success", + "failed": "Failed", + "running": "Running", + "job_id": "Execution Instance ID", + "import_count": "Imported Records", + "create_time": "Created At", + "average_rate": "Average Rate", + "records_per_second": "{{rate}} records/s", + "duration": "Import Duration", + "seconds": "{{seconds}} s", + "status": "Status", + "other": "Details" + }, + "view": { + "title": "View Task", + "unavailable": "Unable to load the current task details.", + "retry": "Retry task details", + "name": "Task Name", + "source_type": "Source Type", + "target_space": "Target Space", + "target_graph": "Target Graph", + "sync_type": "Sync Type" + }, + "edit": { + "name": "Task Name", + "name_placeholder": "Enter task name", + "sync_type": "Sync Type", + "cron_extra": "Quartz expression: seconds minutes hours day month week year (optional)", + "cron_required": "Enter schedule information", + "cron_placeholder": "Schedule * 5 * * * * (*)", + "cron_rule": "Enter a valid Quartz expression", + "update_success": "Updated successfully", + "graphspace": "Graph Space", + "graph": "Graph", + "source": "Data Source", + "source_type": "Source Type", + "basic_info": "Basic Information", + "target": "Target", + "step_basic": "Basic Information", + "step_source_fields": "Select Source Fields", + "step_mapping_fields": "Select Mapping Fields", + "step_schedule": "Schedule", + "graph_not_exist": "Graph does not exist", + "datasource_failed": "Could not load the selected data source.", + "graphspace_failed": "Could not load the selected graph space.", + "request_failed": "Could not validate the task context.", + "retry_context": "Retry validation", + "load_datasources_failed": "Could not load data sources.", + "retry_datasources": "Retry data sources", + "load_graphspaces_failed": "Could not load graph spaces.", + "retry_graphspaces": "Retry graph spaces", + "load_graphs_failed": "Could not load graphs for the selected graph space.", + "retry_graphs": "Retry graphs", + "load_fields_failed": "Could not load fields from the selected data source.", + "retry_fields": "Retry source fields", + "submit_failed": "Could not create the import task. Check the service and retry.", + "graphspace_full": "Graph space {{graphspace}} has reached its storage limit and cannot accept more data.", + "duplicate_check_failed": "Failed to check duplicate task name", + "duplicate_name": "Task name already exists", + "name_rule": "Use Chinese characters, letters, numbers, or underscores, up to 20 characters", + "select_graphspace": "Select graph space", + "select_graph": "Select graph", + "select_source": "Select data source", + "no_datasource": "No data source", + "go_create_datasource": "Create now", + "select_source_type": "Select source type", + "select_source_fields": "Select source fields", + "source_available_fields": "Available Source Fields", + "selected_fields": "Selected Fields", + "delete_field_confirm": "Delete this field?", + "add_field_placeholder": "Add field: letters, numbers, hyphen, and underscore only", + "add_vertex_mapping": "Add Vertex Mapping", + "add_edge_mapping": "Add Edge Mapping", + "type": "Type", + "vertex": "Vertex", + "edge": "Edge", + "mapping_name": "Name", + "mapping_required_tip": "Create at least one vertex or edge mapping", + "create_vertex": "Create Vertex", + "edit_vertex": "Edit Vertex", + "create_edge": "Create Edge", + "edit_edge": "Edit Edge", + "vertex_type": "Vertex Type", + "edge_type": "Edge Type", + "id_column": "ID Column", + "source_id": "Source ID", + "target_id": "Target ID", + "property_mapping": "Property Mapping", + "value_mapping": "Value Mapping", + "select_schema_field": "Select schema field", + "select_mapping_field": "Select mapping field", + "duplicate_property": "Properties cannot be duplicated", + "auto_match": "Auto Match", + "schedule_once": "Run Once", + "schedule_realtime": "Realtime", + "schedule_cron": "Scheduled", + "load_full": "Full", + "load_incremental": "Incremental" + } + }, + "datasource": { + "title": "Data Source Management", + "create": "Add Data Source", + "delete": "Delete Data Source", + "search_placeholder": "Search by data source name", + "delete_title": "Delete Data Source", + "delete_content": "This data source will be removed from the system. Delete this data source?", + "selected_count": "{{selected}} selected / {{total}} total", + "col": { + "name": "Name", + "type": "Type", + "host": "Host", + "port": "Port", + "username": "Username", + "creator": "Creator", + "create_time": "Created At", + "operation": "Actions" + }, + "form": { + "upload": "Upload File", + "no_compress": "None", + "title_create": "Add Data Source", + "title_edit": "Edit Data Source", + "basic_info": "Basic Information", + "config_info": "Configuration", + "auth_info": "Authentication", + "name": "Data Source Name", + "name_placeholder": "Enter data source name", + "name_rule": "Use Chinese characters, letters, numbers, or underscores only", + "type": "Data Source Type", + "select_type": "Select data source type", + "required": "{{label}} is required", + "required_upload": "Upload {{label}}", + "upload_failed": "Upload failed", + "auth_type": "Authentication Type", + "auth_none": "None", + "auth_kerberos": "Kerberos authentication", + "keytab_file": "keytab file", + "conf_file": "conf file", + "local_upload": "Local Upload", + "file_type": "File Type", + "select": "Select", + "header_placeholder": "Enter header, separated by commas", + "delimiter": "Column Delimiter", + "charset": "Charset", + "date_format": "Date Format", + "time_zone": "Time Zone", + "skipped_line": "Skipped Lines", + "compression": "Compression", + "local_file": "Local File", + "server_placeholder": "Enter Kafka server list", + "topic_placeholder": "Enter topic", + "from_beginning": "Read From Beginning", + "db_type": "Database Type", + "select_db_type": "Select database type", + "url_placeholder": "Enter URL", + "url_rule": "Enter a valid JDBC URL, for example: jdbc:mysql://127.0.0.1:3306/db_name", + "database": "Database Name", + "database_placeholder": "Enter database name", + "schema_placeholder": "Enter schema", + "table": "Table Name", + "table_placeholder": "Enter table name", + "username": "Username", + "username_placeholder": "Enter username", + "password": "Password", + "password_placeholder": "Enter password", + "page_size": "Page Size", + "where": "WHERE Clause", + "where_placeholder": "Enter WHERE clause", + "test_connection": "Test Connection", + "connection_success": "Connection succeeded", + "connection_failed": "Connection failed", + "connection_unsupported": "JDBC datasource check is not supported locally" + } + }, + "schema": { + "list_failed": "Schema data could not be loaded. Check the Server connection and retry.", + "retry": "Retry schema data", + "delete_failed": "Delete failed. Check the server connection and retry.", + "title": "Metadata Management", + "identity": { + "graph_unavailable": "Unable to load the current graph.", + "graphspace_unavailable": "Unable to load the current GraphSpace.", + "retry": "Retry graph context" + }, + "name_placeholder": "Letters, numbers, underscores, and Chinese characters are allowed", + "delete_irreversible": "This action cannot be undone. Please proceed with caution.", + "delete_task_hint": "Deleting metadata may take a while. Check Task Management for progress.", + "col": { + "label_index": "Label Index", + "properties": "Properties", + "property_indexes": "Property Index" + }, + "validation": { + "duplicate_index": "Index names cannot be duplicated", + "duplicate_property": "Properties cannot be duplicated" + }, + "common": { + "add_success": "Added successfully", + "batch_delete": "Delete Selected", + "delete_warning": "This action cannot be undone. Please proceed with caution.", + "name_placeholder": "Letters, numbers, underscores, and Chinese characters are allowed" + }, + "options": { + "size_tiny": "Tiny", "size_small": "Small", "size_normal": "Normal", "size_big": "Big", "size_huge": "Huge", + "line_thick": "Thick", "line_fine": "Fine", + "id_primary": "Primary Key ID", "id_auto": "Automatic", "id_string": "Custom String", "id_number": "Custom Number", "id_uuid": "Custom UUID", + "required": "Required", "nullable": "Nullable", + "index_secondary": "Secondary Index", "index_range": "Range Index", "index_search": "Search Index", "index_unique": "Unique Index" + }, + "tab": { + "property": "Property", + "vertex": "Vertex Type", + "edge": "Edge Type", + "vertex_index": "Vertex Index", + "edge_index": "Edge Index" + }, + "property": { + "create": "Create Property", + "delete_confirm": "Confirm deleting this property?", + "in_use": "Properties {{names}} are in use and cannot be deleted.", + "col": { + "name": "Property Name", + "type": "Data Type", + "cardinality": "Cardinality", + "operation": "Actions" + }, + "form": { + "name": "Property Name: ", + "type": "Data Type: ", + "cardinality": "Cardinality: " + } + }, + "vertex": { + "create": "Create Vertex Type", + "edit": "Edit Vertex Type", + "detail_failed": "Vertex type details could not be loaded. Retry before editing.", + "id": "Vertex ID", + "in_use": "Vertices {{names}} are in use and cannot be deleted.", + "select_properties_first": "Select related properties first", + "style": "Vertex Style: ", + "display_fields": "Display Fields", + "delete_confirm": "Confirm deleting this vertex type?", + "delete_warning": "This action cannot be undone. Please proceed with caution.", + "delete_task_note": "Deleting metadata may take a while. Check Task Management for progress.", + "col": { + "name": "Vertex Type Name", + "id_strategy": "ID Strategy", + "properties": "Properties", + "primary_keys": "Primary Keys", + "label_index": "Label Index", + "property_index": "Property Index", + "operation": "Actions" + }, + "form": { + "title_create": "Create Vertex Type", + "title_edit": "Edit Vertex Type", + "name": "Vertex Type Name: ", + "id_strategy": "ID Strategy: ", + "style": "Vertex Style: ", + "display_fields": "Display Fields", + "label_index": "Label Index", + "property_index": "Property Index", + "properties": "Properties" + } + }, + "edge": { + "create": "Create Edge Type", + "edit": "Edit Edge Type", + "detail_failed": "Edge type details could not be loaded. Retry before editing.", + "display_fields": "Display Fields", + "link_multi_times": "Allow Multiple Links", + "parent": "Parent Edge Type: ", + "select_parent": "Select parent edge", + "style": "Edge Style: ", + "delete_confirm": "Confirm deleting this edge type?", + "delete_warning": "This action cannot be undone. Please proceed with caution.", + "delete_task_note": "Deleting metadata may take a while. Check Task Management for progress.", + "col": { + "name": "Edge Type Name", + "type": "Edge Type", + "source": "Source Type", + "target": "Target Type", + "properties": "Properties", + "sort_keys": "Sort Keys", + "label_index": "Label Index", + "property_index": "Property Index", + "operation": "Actions" + }, + "type": { + "normal": "Normal", + "parent": "Parent", + "sub": "Sub", + "NORMAL": "Normal", + "PARENT": "Parent", + "SUB": "Sub", + "parent_label": "Parent: " + }, + "form": { + "title_create": "Create Edge Type", + "title_edit": "Edit Edge Type", + "name": "Edge Type Name: ", + "type": "Type: ", + "type_normal": "Normal", + "type_parent": "Parent", + "type_sub": "Sub", + "parent": "Parent Edge Type: ", + "parent_placeholder": "Select parent edge", + "style": "Edge Style: ", + "source": "Source Type", + "target": "Target Type", + "multi_link": "Allow Multiple Links", + "properties": "Properties", + "sort_keys": "Sort Keys", + "display_fields": "Display Fields", + "label_index": "Label Index", + "property_index": "Property Index" + } + }, + "index": { + "vertex_title": "Vertex Index", + "edge_title": "Edge Index", + "create": "Create Index", + "col": { + "name": "Index Name", + "type": "Index Type", + "fields": "Fields", + "operation": "Actions" + } + }, + "image_view": { + "load_failed": "Schema diagram data could not be loaded. Retry before editing.", + "vertex": "Vertex Type", + "edge": "Edge Type", + "view_properties": "View Properties" + } + }, + "schema_template": { + "title": "{{name}} - Schema Template Management", + "create": "Create Schema Template", + "search_placeholder": "Enter a schema template name", + "delete_confirm": "Delete {{name}}?", + "delete_success": "Deleted successfully", + "delete_failed": "Delete failed", + "create_duplicate": "A schema template named {{name}} already exists. Choose another name.", + "create_failed": "Could not create the schema template. Review the input and retry.", + "update_failed": "Could not update the schema template. Review the input and retry.", + "create_success": "Created successfully", + "update_success": "Updated successfully", + "action": { + "view": "View", + "edit": "Edit", + "create": "Create", + "delete": "Delete" + }, + "column": { + "name": "Schema Template Name", + "created_at": "Created At", + "updated_at": "Updated At", + "creator": "Creator", + "operation": "Actions" + }, + "form": { + "name": "Schema Template Name", + "name_placeholder": "Enter a schema name", + "schema_placeholder": "Enter a schema" + } + }, + "navigation_page": { + "title": "Navigation", + "step1": "Create Data", + "step2": "Business Analysis", + "step3": "System Management", + "step4": "Business Management", + "create_graph": "Create Graph", + "create_schema": "Create Schema", + "load_data": "Load Data", + "graph_query": "Graph Query", + "graph_algorithm": "Graph Algorithm", + "graph_manage": "Graph Space Management", + "data_manage": "Data Management", + "account_manage": "Account Management", + "role_manage": "Role Management", + "resource_manage": "Resource Management", + "task_manage": "Task Management", + "datasource_manage": "Data Source Management", + "operation_manage": "Operations", + "cluster_manage": "Cluster Management", + "monitor_manage": "Monitoring", + "node_manage": "Node Operations", + "alert_manage": "Alert Management", + "dashboard_checking": "Checking Dashboard availability...", + "dashboard_unavailable": "Dashboard is unavailable. Check dashboard.address and service health.", + "dashboard_popup_blocked": "The Dashboard window was blocked. Allow pop-ups and retry." + }, + "not_found": { + "subtitle": "Page not found", + "home": "Return home" + }, + "login": { + "submit": "Sign In", + "username": "Username", + "password": "Password", + "username_required": "Please enter your username.", + "password_required": "Please enter your password.", + "title": "Sign in to HugeGraph" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/index.js b/hugegraph-hubble/hubble-fe/src/i18n/resources/index.js new file mode 100644 index 000000000..c3ae8cf89 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/index.js @@ -0,0 +1,22 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import zhCNResources from './zh-CN'; +import enUSResources from './en-US'; + +export {zhCNResources, enUSResources}; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/index.ts b/hugegraph-hubble/hubble-fe/src/i18n/resources/index.ts deleted file mode 100644 index bad374766..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import zhCNResources from './zh-CN'; -import enUSResources from './en-US'; - -export { zhCNResources, enUSResources }; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/ERView.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/ERView.json new file mode 100644 index 000000000..80e98df8c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/ERView.json @@ -0,0 +1,30 @@ +{ + "ERView": { + "edge": { + "name": "边", + "type": "边类型", + "start": "起点ID", + "end": "终点ID", + "create": "新增边", + "e1name": "边1", + "e2name": "边2", + "out": "出边", + "in": "入边", + "both": "双边" + }, + "vertex": { + "name": "顶点", + "type": "顶点类型", + "create": "新增顶点", + "v1name": "顶点1", + "v2name": "顶点2" + }, + "control": { + "zoom_in": "放大", + "zoom_out": "缩小", + "undo": "撤销", + "redo": "重做", + "auto_map": "自动映射" + } + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/board.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/board.json new file mode 100644 index 000000000..fd84c8a7f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/board.json @@ -0,0 +1,15 @@ +{ + "Topbar": { + "exit":{ + "name": "退出登录", + "confirm":"确定退出吗?", + "success":"退出成功" + } + }, + "selector": { + "placeholder": "请选择" + }, + "navigation": { + "name": "导航" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/common.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/common.json new file mode 100644 index 000000000..5e3147c27 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/common.json @@ -0,0 +1,37 @@ +{ + "common" : { + "verify" : { + "ok": "确定", + "cancel": "取消", + "yes": "是", + "no": "否" + }, + "status": { + "new": "新建", + "running": "执行中", + "success": "完成", + "cancelling": "停止", + "cancelled": "停止", + "failed": "失败", + "undefined": "未知" + }, + "validation": { + "required": "必填项", + "max": "最大长度为{{max}}个字符!", + "invalid_ip": "不是合法的IP", + "invalid_port": "不是合法的端口", + "invalid_cron": "不是合法的quartz格式", + "name_rule": "以字母开头,只能包含小写字母、数字、_", + "cn_name_rule": "只能包含中文、字母、_", + "property_name_rule": "只能包含中文、字母、数字、_", + "normal_name_rule": "只能包含中文、字母、数字、_, 不能超过20个字符", + "jdbc_rule": "请输入正确的jdbc url, 例如:jdbc:mysql://127.0.0.1:3306/db_name", + "account_name_rule": "账号名不超过16个字符,且不能以下划线开始和结尾", + "invalid_data_format": "非法的数据格式" + } + }, + "request": { + "failed": "请求失败", + "error": "请求出错:{{message}},path:{{path}}" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/index.js b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/index.js new file mode 100644 index 000000000..30ff1ae6f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/index.js @@ -0,0 +1,26 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import Common from './common.json'; +import Board from './board.json'; +import ERView from './ERView.json'; +export { + Common, + Board, + ERView, +}; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/AsyncTasks.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/AsyncTasks.json deleted file mode 100644 index ce2848f4c..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/AsyncTasks.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "async-tasks": { - "title": "任务管理", - "placeholders": { - "search": "请输入任务ID或名称搜索" - }, - "table-column-title": { - "task-id": "任务ID", - "task-name": "任务名称", - "task-type": "任务类型", - "create-time": "创建时间", - "time-consuming": "耗时", - "status": "状态", - "manipulation": "操作" - }, - "table-filters": { - "task-type": { - "all": "全部", - "gremlin": "Gremlin任务", - "algorithm": "算法任务", - "remove-schema": "删除元数据", - "create-index": "创建索引", - "rebuild-index": "重建索引" - }, - "status": { - "all": "全部", - "scheduling": "调度中", - "scheduled": "排队中", - "queued": "排队中", - "running": "运行中", - "restoring": "恢复中", - "success": "成功", - "failed": "失败", - "cancelling": "已取消", - "cancelled": "已取消" - } - }, - "table-selection": { - "selected": "已选{{number}}项", - "delete-batch": "批量删除" - }, - "manipulations": { - "abort": "终止", - "aborting": "终止中", - "delete": "删除", - "check-result": "查看结果", - "check-reason": "查看原因" - }, - "hint": { - "delete-confirm": "删除确认", - "delete-description": "是否确认删除该任务?删除后无法恢复,请谨慎操作", - "delete-succeed": "删除成功", - "delete-batch-confirm": "批量删除", - "delete-batch-description": "确认删除以下任务?删除后无法恢复,请谨慎操作", - "delete": "删除", - "cancel": "取消", - "no-data": "暂无数据", - "empty": "您暂时还没有任何任务", - "select-disabled": "任务{{id}}无法被选中删除", - "check-details": "查看详情", - "creation-failed": "创建失败" - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/GraphManagementSidebar.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/GraphManagementSidebar.json deleted file mode 100644 index d6b2f5455..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/GraphManagementSidebar.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "data-import": "数据导入", - "import-task": "导入任务", - "map-templates": "映射模板" -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/addition.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/addition.json deleted file mode 100644 index d137ee317..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/addition.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "addition": { - "function-parameter": { - "edge-type": "边类型", - "vertex-id": "顶点ID" - }, - "store": { - "required": "必填项", - "item-is-required": "此项为必填项", - "no-match-input-requirements": "不符合输入要求", - "rule1": "请输入字母、数字或特殊字符", - "rule2": "请输入范围在 1-65535 的数字", - "rule3": "必须同时填写用户名和密码", - "rule4": "必须为中英文,数字和下划线", - "illegal-data-format": "非法的数据格式", - "incorrect-time-format": "时间格式不正确", - "cannot-be-empty": "此项不能为空", - "cannot-be-empty1": "该项不能为空", - "same-edge-name-notice": "存在同名边,请输入其它名称", - "same-vertex-name-notice": "存在同名顶点,请输入其它名称", - "same-property-name-notice": "存在同名属性,请输入其它名称", - "same-index-name-notice": "存在同名属性索引,请输入其它名称", - "network-error": "网络异常,请稍后重试" - }, - "constant": { - "primary-key-id": "主键ID", - "automatic-generation": "自动生成", - "custom-string": "自定义字符串", - "custom-number": "自定义数字", - "custom-uuid": "自定义UUID", - "greater-than": "大于", - "greater-than-or-equal": "大于等于", - "less-than": "小于", - "less-than-or-equal": "小于等于", - "equal": "等于" - }, - "menu": { - "chart": "图", - "table": "表格", - "task-id": "任务ID", - "list-mode": "列表模式", - "chart-mode": "图模式", - "secondary-index": "二级索引", - "range-index": "范围索引", - "full-text-index": "全文索引", - "type-index": "类型索引", - "base-info": "基础信息", - "select-reuse-item": "选择复用项", - "confirm-reuse-item": "确认复用项", - "complete-reuse": "完成复用" - }, - "vertex": { - "type-detail": "顶点类型详情", - "edit-type": "编辑顶点类型", - "using-cannot-delete": "当前顶点类型正在使用中,不可删除。", - "using-cannot-delete-confirm": "使用中顶点类型不可删除,确认删除以下未使用顶点类型?", - "del-vertex-confirm": "确认删除此顶点类型?", - "del-vertex-confirm-again": "确认删除此顶点类型?删除后无法恢复,请谨慎操作", - "vertex-type-name": "顶点类型名称", - "vertex-style": "顶点样式", - "vertex-display-content": "顶点展示内容", - "select-vertex-display-content-placeholder": "请选择顶点展示内容", - "create-vertex-type": "创建顶点类型", - "vertex-index": "顶点索引", - "corresponding-vertex-type": "对应顶点类型", - "no-vertex-type-desc": "您暂时还没有任何顶点类型,立即创建" - }, - "edge": { - "display-content": "边展示内容", - "display-content-select-desc": "请选择边展示内容", - "index-info": "索引信息", - "index-name": "索引名称", - "index-type": "索引类型", - "edge-index": "边索引", - "index-type-select-desc": "请选择索引类型", - "property-select-desc": "请选择属性", - "add-group": "新增一组", - "confirm-del-edge-type": "确认删除此边类型?", - "confirm-del-edge-type-again": "确认删除边类型?删除后无法恢复,请谨慎操作", - "confirm-del-edge-careful-notice": "删除后无法恢复,请谨慎操作", - "no-edge-desc": "您暂时还没有任何边类型,立即创建", - "create-edge": "创建边类型", - "multiplexing-existing-type": "复用已有类型", - "multiplexing-edge-type": "复用边类型", - "multiplexing-edge-type-notice": "边类型关联的属性和属性索引、起点类型和终点类型及其关联的属性和属性索引将一同复用", - "verification-result": "校验结果", - "be-verified": "待校验", - "verified-again": "重新校验" - }, - "message": { - "no-can-delete-vertex-type": "无可删除顶点类型", - "no-index-notice": "您暂时还没有任何索引", - "property-create-desc": "您暂时还没有任何属性,立即创建", - "del-unused-property-notice": "使用中属性不可删除,确认删除以下未使用属性?", - "please-enter-keywords": "请输入搜索关键字", - "no-property-can-delete": "无可删除属性", - "no-metadata-notice": "您暂时还没有任何元数据", - "no-vertex-or-edge-notice": "您还未设置顶点类型或边类型", - "no-adjacency-points": "不存在邻接点", - "no-more-points": "不存在更多邻接点", - "please-enter-number": "请输入数字", - "please-enter-string": "请输入字符串", - "please-enter": "请输入", - "no-chart-desc": "无图结果,请查看表格或Json数据", - "no-data-desc": "暂无数据结果", - "data-loading": "数据加载中", - "submit-async-task": "提交异步任务中", - "submit-success": "提交成功", - "submit-fail": "提交失败", - "task-submit-fail": "任务提交失败", - "fail-reason": "失败原因", - "fail-position": "失败位置", - "selected": "已选", - "edge-del-confirm": "确认删除以下边?", - "long-time-notice": "删除元数据耗时较久,详情可在任务管理中查看。", - "index-long-time-notice": "创建索引可能耗时较久,详情可在任务管理中查看", - "property-del-confirm": "确认删除此属性?", - "property-del-confirm-again": "确认删除此属性?删除后无法恢复,请谨慎操作", - "index-del-confirm": "删除索引后,无法根据此属性索引进行查询,请谨慎操作。", - "edge-name-rule": "允许出现中英文、数字、下划线", - "source-type-select-placeholder": "请选择起点类型", - "target-type-select-placeholder": "请选择终点类型", - "select-distinguishing-key-property-placeholder": "请选择区分键属性", - "select-association-key-property-placeholder": "请先选择关联属性", - "index-open-notice": "开启索引会影响使用性能,请按需开启", - "duplicate-name": "有重名", - "dependency-conflict": "依赖冲突", - "already-exist": "已存在", - "pass": "通过", - "select-reuse-graph-placeholder": "请选择要复用的图", - "reuse-complete": "复用完成", - "vertex-type-reuse-success": "已成功复用顶点类型", - "property-using-cannot-delete": "当前属性数据正在使用中,不可删除。", - "reuse-property-success": "已成功复用属性", - "reuse-vertex-type-notice": "顶点类型关联的属性和属性索引将一同复用", - "illegal-vertex": "该顶点是非法顶点,可能是由悬空边导致" - }, - "operate": { - "reuse-vertex-type": "复用顶点类型", - "reuse-existing-property": "复用已有属性", - "reuse-property": "复用属性", - "create-property": "创建属性", - "continue-reuse": "继续复用", - "back-to-view": "返回查看", - "complete": "完成", - "next-step": "下一步", - "previous-step": "上一步", - "rename": "重命名", - "del-ing": "删除中", - "view-task-management": "去任务管理查看", - "batch-del": "批量删除", - "multiplexing": "复用", - "de-multiplexing": "取消复用", - "open": "开", - "close": "关", - "look": "查看", - "view-property": "查看属性", - "filter": "筛选", - "add-filter-item": "添加属性筛选", - "enlarge": "放大", - "narrow": "缩小", - "center": "居中", - "download": "下载", - "exit-full-screen": "退出全屏", - "full-screen": "全屏", - "load-background": "加载背景", - "load-spinner": "加载 spinner", - "rendering": "正在渲染", - "detail": "详情", - "favorite": "收藏", - "favorite-success": "收藏成功", - "load-statement": "加载语句", - "time": "时间", - "name": "名称", - "favorite-statement": "收藏语句", - "favorite-desc": "请输入收藏名称", - "operate": "操作", - "query": "查询", - "hidden": "隐藏", - "modify-name": "修改名称", - "execution-record": "执行记录", - "favorite-queries": "收藏的查询", - "favorite-search-desc": "搜索收藏名称或语句", - "expand-collapse": "展开/收起", - "expand": "展开", - "collapse": "收起", - "favorite-del-desc": "是否确认删除该条收藏语句?", - "input-query-statement": "请输入查询语句", - "query-statement-required": "查询语句不能为空", - "execute-query": "执行查询", - "execute-task": "执行任务", - "execute-ing": "执行中", - "clean": "清空", - "modify-success": "修改成功", - "query-result-desc": "查询模式适合30秒内可返回结果的小规模分析;任务模式适合较长时间返回结果的大规模分析,任务详情可在任务管理中查看" - }, - "common": { - "in-use": "使用中", - "not-used": "未使用", - "status": "状态", - "no-result": "无结果", - "cardinal-number": "基数", - "allow-null": "允许为空", - "allow-multiple-connections": "允许多次连接", - "multiple-connections-notice": "开启后两顶点间允许存在多条该类型的边", - "term": "项", - "in-edge": "入边", - "add-in-edge": "添加入边", - "out-edge": "出边", - "add-out-edge": "添加出边", - "add": "添加", - "value": "值", - "null-value": "空值", - "id-strategy": "ID策略", - "id-value": "ID值", - "id-input-desc": "请输入ID值", - "please-input": "请输入", - "add-success": "添加成功", - "add-fail": "添加失败", - "vertex-type": "顶点类型", - "selected-vertex-type": "已选顶点类型", - "vertex-id": "顶点ID", - "vertex-name": "顶点名称", - "illegal-vertex-desc": "该顶点是非法顶点,可能是由悬空边导致", - "add-vertex": "添加顶点", - "vertex-type-select-desc": "请选择顶点类型", - "edge-type": "边类型", - "selected-edge-type": "已选边类型", - "edge-style": "边样式", - "modify-edge-type": "编辑边类型", - "edge-type-detail": "边类型详情", - "edge-type-name": "边类型名称", - "edge-direction": "边方向", - "edge-type-select-desc": "请选择边类型", - "edge-id": "边ID", - "edge-name": "边名称", - "source": "起点", - "source-type": "起点类型", - "target": "终点", - "target-type": "终点类型", - "rule": "规则", - "property": "属性", - "selected-property": "已选属性", - "property-name": "属性名称", - "add-property": "添加属性", - "association-property": "关联属性", - "association-property-and-type": "关联属性及类型", - "property-index": "属性索引", - "selected-property-index": "已选属性索引", - "property-index-name": "属性索引名称", - "property-value": "属性值", - "required-property": "不可空属性", - "nullable-property": "可空属性", - "primary-key": "主键", - "primary-key-property": "主键属性", - "select-primary-key-property-placeholder": "请选择主键属性", - "distinguishing-key": "区分键", - "distinguishing-key-property": "区分键属性", - "property-input-desc": "请输入属性值", - "del-comfirm": "确认删除", - "ask": "吗?", - "confirm": "确认", - "del-success": "删除成功", - "save-scuccess": "保存成功", - "save-fail": "保存失败", - "save": "保存", - "cancel": "取消", - "more": "更多", - "edit": "编辑", - "del": "删除", - "fold": "折叠", - "open": "展开", - "close": "关闭", - "required": "必填项", - "format-error-desc": "必须以字母开头,允许出现英文、数字、下划线", - "no-matching-results": "无匹配结果", - "no-data": "暂无数据", - "data-type": "数据类型", - "corresponding-type": "对应类型" - }, - "appbar": { - "graph-manager": "图管理" - }, - "graphManagementHeader": { - "graph-manager": "图管理", - "community": "社区版", - "business": "商业版", - "limit-desc": "支持图上限", - "limit-desc1": "图磁盘上限", - "individual": "个", - "input-placeholder": "搜索图名称或ID", - "graph-create": "创建图" - }, - "graphManagementList": { - "save-scuccess": "保存成功", - "graph-del": "删除图", - "graph-del-comfirm-msg": "删除后该图所有配置均不可恢复", - "graph-edit": "编辑图", - "id": "图ID", - "id-desc": "为创建的图设置唯一标识的ID", - "name": "图名称", - "name-desc": "填写需要连接的图的名称", - "host": "主机名", - "port": "端口号", - "port-desc": "请输入端口号", - "username": "用户名", - "password": "密码", - "creation-time": "创建时间", - "visit": "访问" - }, - "graphManagementEmptyList": { - "graph-create": "创建图", - "graph-create-desc": "您暂时还没有任何图,立即创建", - "no-matching-results": "无匹配结果" - }, - "newGraphConfig": { - "graph-create": "创建图", - "create": "创建", - "create-scuccess": "创建成功", - "id": "图ID", - "id-desc": "为创建的图设置唯一标识的ID", - "name": "图名称", - "name-desc": "填写需要连接的图的名称", - "host": "主机名", - "host-desc": "请输入主机名", - "port": "端口号", - "port-desc": "请输入端口号", - "username": "用户名", - "password": "密码", - "not-required-desc": "未设置则无需填写" - }, - "graphManagementSidebar": { - "data-analysis": "数据分析", - "graph-select": "选择图", - "metadata-config": "元数据配置", - "data-import": "数据导入", - "task-management": "任务管理" - }, - "dataAnalyze": { - "cannot-access": "无法访问", - "return-home": "返回首页" - }, - "dataAnalyzeInfoDrawer": { - "edit-details": "编辑详情", - "data-details": "数据详情" - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/common.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/common.json deleted file mode 100644 index 98d821703..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/common.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "common": { - "loading-data": "数据加载中" - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/data-import/import-tasks/ImportTasks.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/data-import/import-tasks/ImportTasks.json deleted file mode 100644 index 2edf4240c..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/data-import/import-tasks/ImportTasks.json +++ /dev/null @@ -1,343 +0,0 @@ -{ - "breadcrumb": { - "first": "导入任务", - "second": "任务详情" - }, - "import-manager": { - "hint": { - "empty-task": "您暂时还没有任何任务,立即创建", - "no-result": "无结果", - "creation-succeed": "创建成功", - "update-succeed": "编辑成功" - }, - "manipulation": { - "create": "创建" - }, - "placeholder": { - "input-job-name": "请输入任务名称搜索", - "input-valid-job-name": "允许出现中英文、数字、下划线", - "input-job-description": "请输入任务备注" - }, - "list-column-title": { - "job-name": "任务名称", - "size": "大小", - "create-time": "创建时间", - "status": "状态", - "time-consuming": "任务耗时", - "manipulation": "操作" - }, - "list-column-status": { - "DEFAULT": "未开始", - "UPLOADING": "未开始", - "MAPPING": "设置中", - "SETTING": "导入中", - "LOADING": "导入中", - "FAILED": "失败", - "SUCCESS": "成功" - }, - "list-column-manipulations": { - "start": "开始任务", - "resume-setting": "继续设置", - "resume-importing": "继续导入", - "check-error-log": "查看原因", - "delete": "删除" - }, - "modal": { - "create-job": { - "title": "创建导入任务", - "job-name": "任务名称:", - "job-description": "任务备注:" - }, - "delete-job": { - "title": "删除任务", - "hint": "确认删除任务{{name}}吗?", - "sub-hint": "删除后该任务不可恢复" - }, - "manipulations": { - "create": "创建", - "delete": "删除", - "cancel": "取消" - } - }, - "validator": { - "no-empty": "此项为必填项", - "over-limit-size": "已超出字数限制", - "invalid-format": "请输入中英文、数字、下划线" - } - }, - "import-job-details": { - "tabs": { - "basic-settings": "基础设置", - "uploaded-files": "上传文件", - "data-maps": "映射文件", - "import-details": "导入详情" - }, - "basic": { - "job-name": "任务名称:", - "job-description": "任务备注:", - "modal": { - "edit-job": { - "title": "编辑导入任务", - "job-name": "任务名称:", - "job-description": "任务备注:" - }, - "manipulations": { - "save": "保存", - "cancel": "取消" - } - } - }, - "manipulations": { - "edit": "编辑", - "resume-task": "继续任务" - } - }, - "step": { - "first": "上传文件", - "second": "设置数据映射", - "third": "导入数据", - "fourth": "完成" - }, - "upload-files": { - "click": "点击", - "description": "点击或将文件拖拽到此处上传,可同时上传多个支持csv文件,单个1G以内,单次累计10G以内", - "drag": "拖拽", - "description-1": "或将文件", - "description-2": "到此处上传,可同时上传多个支持csv文件,单个1G以内,单次累计10G以内", - "cancel": "取消上传", - "next": "下一步", - "wrong-format": "仅支持 csv 格式文件", - "over-single-size-limit": "容量已超过 1 GB", - "over-all-size-limit": "总容量已超过 10 GB", - "empty-file": "文件为空,请重新上传", - "no-duplicate": "下列上传的文件已存在:" - }, - "data-configs": { - "file": { - "title": "文件设置", - "include-header": "包含表头", - "delimiter": { - "title": "分隔符", - "comma": "逗号", - "semicolon": "分号", - "tab": "制表符", - "space": "空格", - "custom": "自定义" - }, - "code-type": { - "title": "编码格式", - "UTF-8": "UTF-8", - "GBK": "GBK", - "ISO-8859-1": "ISO-8859-1", - "US-ASCII": "US-ASCII", - "custom": "自定义" - }, - "date-type": { - "title": "日期格式", - "custom": "自定义" - }, - "skipped-line": "跳过行", - "timezone": "时区", - "save": "保存", - "placeholder": { - "input-delimiter": "请输入分隔符", - "input-charset": "请输入编码格式", - "input-date-format": "请输入日期格式" - }, - "hint": { - "save-succeed": "已保存文件设置" - } - }, - "type": { - "title": "类型设置", - "basic-settings": "基础设置", - "manipulation": { - "create": "创建", - "save": "保存", - "cancel": "取消", - "create-vertex": "创建顶点映射", - "create-edge": "创建边映射" - }, - "info": { - "type": "类型", - "name": "名称", - "ID-strategy": "ID策略", - "edit": "编辑", - "delete": "删除" - }, - "ID-strategy": { - "PRIMARY_KEY": "主键ID", - "AUTOMATIC": "自动生成", - "CUSTOMIZE_STRING": "自定义字符串", - "CUSTOMIZE_NUMBER": "自定义数字", - "CUSTOMIZE_UUID": "自定义UUID" - }, - "vertex": { - "title": "创建顶点映射", - "type": "顶点类型", - "ID-strategy": "ID策略", - "ID-column": "ID列", - "map-settings": "映射设置", - "add-map": { - "title": "添加映射", - "name": "列名", - "sample": "数据样例", - "property": "映射属性", - "clear": "清空" - }, - "select-all": "全选", - "advance": { - "title": "高级设置", - "nullable-list": { - "title": "空值列表", - "empty": "空值", - "custom": "自定义" - }, - "map-property-value": { - "title": "属性值映射", - "add-value": "添加属性值映射", - "fields": { - "property": "属性", - "value-map": "值映射", - "add-value-map": "添加值映射" - } - }, - "placeholder": { - "input": "请选择", - "input-property": "请选择输入属性", - "input-file-value": "请输入文件值", - "input-graph-value": "请输入图导入值" - } - } - }, - "edge": { - "title": "创建边映射", - "type": "边类型", - "source-ID-strategy": "起点ID策略", - "target-ID-strategy": "终点ID策略", - "ID-column": "ID列", - "map-settings": "映射设置", - "add-map": { - "title": "添加映射", - "name": "列名", - "sample": "数据样例", - "property": "映射属性", - "clear": "清空" - }, - "select-all": "全选", - "advance": { - "title": "高级设置", - "nullable-list": { - "title": "空值列表", - "empty": "空值", - "custom": "自定义" - }, - "map-property-value": { - "title": "属性值映射", - "add-value": "添加 属性值映射", - "fields": { - "property": "属性", - "value-map": "值映射", - "add-value-map": "添加值映射" - } - }, - "placeholder": { - "input": "请选择映射属性", - "input-property": "请选择输入属性", - "input-file-value": "请输入文件值", - "input-graph-value": "请输入图导入值" - } - } - }, - "hint": { - "lack-support-for-automatic": "自动生成的ID策略不支持可视化导入,请调用API实现", - "no-vertex-or-edge-mapping": "如下文件未设置顶点类型或边类型映射:" - }, - "placeholder": { - "select-vertex-type": "请选择顶点类型", - "select-edge-type": "请选择边类型", - "select-id-column": "请选择 ID 列", - "empty-value": "空" - } - }, - "manipulations": { - "previous": "上一步", - "next": "下一步", - "add": "添加", - "edit": "编辑", - "delete": "删除", - "cancel": "取消", - "hints": { - "delete-confirm": "确认删除?", - "warning": "删除后无法恢复,请谨慎操作" - } - }, - "validator": { - "no-empty": "该项不能为空" - } - }, - "server-data-import": { - "import-settings": { - "title": "导入设置", - "checkIfExist": "检查边连接的顶点是否存在", - "requestTimesWhenInterpolationFailed": "插入失败重试次数", - "maximumAnalyzedErrorRow": "允许最大解析错误行数", - "requestTicksWhenInterpolationFailed": "插入失败重试间隔/s", - "maxiumInterpolateErrorRow": "允许最大插入错误行数", - "InterpolationTimeout": "插入超时时间/s" - }, - "import-details": { - "title": "导入详情", - "column-titles": { - "file-name": "文件名称", - "type": "类型", - "import-speed": "导入速度", - "import-progress": "导入进度", - "status": "状态", - "time-consumed": "耗时", - "manipulations": "操作" - }, - "content": { - "vertex": "顶点", - "edge": "边" - }, - "status": { - "RUNNING": "运行中", - "SUCCEED": "成功", - "FAILED": "失败", - "PAUSED": "已暂停", - "STOPPED": "已终止" - }, - "manipulations": { - "pause": "暂停", - "resume": "继续", - "retry": "重试", - "abort": "终止", - "failed-cause": "查看原因" - } - }, - "hint": { - "check-vertex": "开启检查会影响导入性能,请按需开启", - "no-data": "正在请求导入", - "confirm-navigation": "确认跳转到任务列表?正在上传的文件可能会丢失" - }, - "validator": { - "no-empty": "该项不能为空", - "need-integer-with-negative": "请输入-1或大于0的整数", - "need-integer": "请输入大于0的整数" - }, - "manipulations": { - "previous": "上一步", - "start": "开始导入", - "cancel": "取消导入", - "finished": "完成" - } - }, - "data-import-status": { - "finished": "导入完成", - "success": "已成功导入 {{number}} 个文件", - "pause": "{{number}} 个文件暂停", - "abort": "{{number}} 个文件终止", - "move-to-import-manager": "返回导入任务列表" - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/dataAnalyze.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/dataAnalyze.json deleted file mode 100644 index 8f01b76e3..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/dataAnalyze.json +++ /dev/null @@ -1,654 +0,0 @@ -{ - "data-analyze": { - "category": { - "gremlin-analyze": "Gremlin 分析", - "algorithm-analyze": "算法分析" - }, - "manipulations": { - "execution": "执行", - "favorite": "收藏", - "reset": "重置" - }, - "hint": { - "graph-disabled": "该图不可用" - }, - "algorithm-list": { - "title": "算法目录", - "loop-detection": "环路检测", - "focus-detection": "交点检测", - "shortest-path": "最短路径", - "shortest-path-all": "全最短路径", - "all-path": "所有路径", - "model-similarity": "模型相似度算法", - "neighbor-rank": "Neighbor Rank推荐算法", - "k-step-neighbor": "k步邻居", - "k-hop": "k跳算法", - "custom-path": "自定义路径", - "radiographic-inspection": "射线检测", - "same-neighbor": "共同邻居", - "weighted-shortest-path": "带权最短路径", - "single-source-weighted-shortest-path": "单源带权最短路径", - "jaccard": "Jaccard相似度", - "personal-rank": "Personal Rank推荐算法" - }, - "algorithm-forms": { - "loop-detection": { - "options": { - "source": "起点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "label": "边类型:", - "max_degree": "最大度数:", - "source_in_ring": "环路包含起点:", - "limit": "返回可达路径最大值:", - "capacity": "访问顶点最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "input-positive-integer": "请填写大于0的整数", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "focus-detection": { - "options": { - "source": "起点ID:", - "target": "终点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "label": "边类型:", - "max_degree": "最大度数:", - "capacity": "访问顶点最大值:", - "limit": "返回交点最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-target-id": "请输入终点ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "input-positive-integer": "请填写大于0的整数", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "shortest-path": { - "options": { - "source": "起点ID:", - "target": "终点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "label": "边类型:", - "max_degree": "最大度数:", - "skip_degree": "超级顶点度数:", - "capacity": "访问顶点最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-target-id": "请输入终点ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-integer": "请填写大于等于0的整数,默认为0", - "input-positive-integer": "请填写大于0的整数", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目", - "skip-degree": "填写查询过程中需要跳过的顶点的最小的边数目,即当顶点的边数目大于超级顶点度数时,跳过该顶点,可用于规避超级点" - }, - "validations": { - "no-empty": "该项不能为空", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数", - "integer-only": "请填写大于等于0的整数", - "postive-integer-only": "请填写大于0的整数" - } - }, - "shortest-path-all": { - "options": { - "source": "起点ID:", - "target": "终点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "label": "边类型:", - "max_degree": "最大度数:", - "capacity": "访问顶点最大值:", - "skip_degree": "超级顶点度数:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-target-id": "请输入终点ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer": "请填写大于0的整数", - "input-integer": "请填写大于等于0的整数,默认为0", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目", - "skip-degree": "填写查询过程中需要跳过的顶点的最小的边数目,即当顶点的边数目大于超级顶点度数时,跳过该顶点,可用于规避超级点" - }, - "validations": { - "no-empty": "该项不能为空", - "integer-only": "请填写大于等于0的整数", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "all-path": { - "options": { - "source": "起点ID:", - "target": "终点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "label": "边类型:", - "max_degree": "最大度数:", - "capacity": "访问顶点最大值:", - "limit": "返回路径最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-target-id": "请输入终点ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "input-positive-integer": "请填写大于0的整数", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "model-similarity": { - "options": { - "method": "起点选择方式:", - "source": "起点ID:", - "vertex-type": "顶点类型:", - "vertex-property": "顶点属性及值:", - "direction": "方向:", - "least_neighbor": "最少邻居数:", - "similarity": "相似度:", - "label": "边类型:", - "max_similar": "相似度最高个数:", - "least_similar": "模形相似点最小个数:", - "property_filter": "属性过滤:", - "least_property_number": "最小属性值个数:", - "max_degree": "最大度数:", - "capacity": "访问顶点最大值:", - "skip_degree": "返回顶点最大值:", - "limit": "返回结果最大值:", - "return_common_connection": "返回共同关联点:", - "return_complete_info": "返回顶点完整信息:" - }, - "radio-value": { - "specific-id": "指定ID", - "filtered-type-property": "筛选类型属性" - }, - "placeholder": { - "input-source-id": "请输入起点ID,多个ID用逗号分隔", - "input-vertex-type": "请选择顶点类型", - "select-vertex-property": "请选择顶点属性", - "input-vertex-property": "多属性值以逗号分隔", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "input-integer": "请填写大于等于0的整数", - "input-positive-integer": "请填写大于0的整数", - "input-integer-gt-1": "请填写大于1的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数", - "input-filtered-property": "请选择需要过滤的属性", - "no-properties": "无属性", - "no-vertex-type": "无顶点类型", - "similarity": "请输入(0-1]的数字" - }, - "hint": { - "vertex_type_or_property": "顶点类型/顶点属性至少填写一项", - "least_property_number": "属性过滤和最小属性值个数需一起使用;设置后效果为:当起点跟其所有的梭形相似点某个属性的值大于等于最小属性值个数时,才会返回该起点及其梭形相似点", - "max-degree": "查询过程中,单个顶点的最大边数目", - "least_neighbor": "邻居数少于当前设定值,则认为起点没有梭形相似点", - "similarity": "起点与\"梭形相似点\"的共同邻居数目占起点的全部邻居数目的比例", - "max_similar": "返回起点的梭形相似点中相似度最高的top个数,0表示全部", - "return_common_connection": "是否返回起点及其\"梭形相似点\"共同关联的中间点" - }, - "validations": { - "no-empty": "该项不能为空", - "no-edge-typs": "无边类型", - "integer-only": "请填写大于等于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数", - "postive-integer-only": "请填写大于0的整数", - "integer-gt-1": "请填写大于1的整数", - "similarity": "请输入(0-1]的数字", - "no-gt-1000": "该值不能大于等于1000" - }, - "add": "添加", - "delete": "删除", - "pre-value": "全部" - }, - "neighbor-rank": { - "options": { - "source": "起点ID:", - "alpha": "Alpha:", - "direction": "方向:", - "capacity": "访问顶点最大值:", - "label": "边类型:", - "degree": "最大度数:", - "top": "每层保留权重Top N:", - "steps": "steps:" - }, - "placeholder": { - "input-source-id": "请输入起点ID", - "input-integer-lt-1000": "请填写大于等于0小于1000的整数, 默认为100", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer": "请填写大于0的整数", - "range": "范围(0-1]" - }, - "hint": { - "top": "在结果中每一层只保留权重最高的N个结果", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "no-edge-typs": "无边类型", - "range": "请填写大于0且小于等于1的数值", - "integer-only-lt-1000": "请填写大于等于0的整数小于1000的整数", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - }, - "pre-value": "全部", - "add-new-rule": "添加规则" - }, - "k-step-neighbor": { - "options": { - "source": "起点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "label": "边类型:", - "max_degree": "最大度数:", - "limit": "返回顶点最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "input-positive-integer": "请填写大于0的整数", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "k-hop": { - "options": { - "source": "起点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "nearest": "最短路径:", - "label": "边类型:", - "max_degree": "最大度数:", - "capacity": "遍历中访问顶点最大值:", - "limit": "返回顶点最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "input-positive-integer": "请填写大于0的整数", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目", - "shortest-path": "开启后,则查询出起始顶点最短路径为depth步的顶点,关闭后,则查询出起始顶点路径为depth步的顶点,可能有环,且不一定是最短路径" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "custom-path": { - "options": { - "method": "起点选择方式:", - "source": "起点ID:", - "vertex-type": "顶点类型:", - "vertex-property": "顶点属性:", - "sort_by": "路径权重排序:", - "capacity": "遍历中访问顶点最大值:", - "limit": "返回顶点最大值:", - "direction": "方向:", - "labels": "边类型:", - "properties": "边属性:", - "weight_by": "根据属性计算边权重:", - "degree": "最大度数:", - "sample": "采样值:", - "steps": "steps:" - }, - "placeholder": { - "input-source-id": "请输入起点ID,多个ID用逗号分隔", - "select-vertex-type": "请选择顶点类型", - "select-vertex-property": "请选择顶点属性", - "input-multiple-properties": "多属性值以逗号分隔", - "input-integer": "请填写大于等于0的整数", - "input-positive-integer": "请填写大于0的整数", - "input-positive-integer-or-negative-one-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "input-property": "请输入边属性", - "input-number": "请输入浮点数字", - "select-edge-type": "请选择边类型", - "select-edge-property": "请选择边属性", - "select-property": "请选择属性", - "no-vertex-type": "无顶点类型", - "no-vertex-property": "无顶点属性", - "no-edge-type": "无边类型", - "no-edge-property": "无边属性", - "no-properties": "无属性" - }, - "hint": { - "top": "在结果中每一层只保留权重最高的N个结果", - "vertex_type_or_property": "顶点类型/顶点属性至少填写一项" - }, - "radio-value": { - "specific-id": "指定ID", - "filtered-type-property": "筛选类型属性", - "none": "不排序", - "ascend": "升序", - "descend": "降序" - }, - "validations": { - "no-empty": "该项不能为空", - "no-edge-typs": "无边类型", - "range": "请填写大于0且小于等于1的数值", - "integer-only": "请填写大于等于0的整数", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数", - "input-number": "请输入浮点数字" - }, - "custom-weight": "自定义权重值", - "add": "添加", - "delete": "删除", - "add-new-rule": "添加规则" - }, - "radiographic-inspection": { - "options": { - "source": "起点ID:", - "direction": "方向:", - "max_depth": "最大步数:", - "label": "边类型:", - "max_degree": "最大度数:", - "capacity": "遍历中访问顶点最大值:", - "limit": "返回非环路路径最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-positive-integer": "请填写大于0的整数", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10000000", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "same-neighbor": { - "options": { - "vertex": "顶点1:", - "other": "顶点2:", - "direction": "方向:", - "label": "边类型:", - "max_degree": "最大度数:", - "limit": "返回共同邻居最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入顶点ID", - "input-other-id": "请输入不同于顶点1的ID", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer": "请填写大于0的整数", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数", - "no-same-value-with-other": "不能与顶点2相同", - "no-same-value-with-vertex": "不能与顶点1相同" - } - }, - "weighted-shortest-path": { - "options": { - "source": "起点ID:", - "target": "终点ID:", - "direction": "方向:", - "weight": "权重属性:", - "with_vertex": "返回顶点完整信息:", - "label": "边类型:", - "max_degree": "最大度数:", - "skip_degree": "跳过点的度数:", - "capacity": "遍历中访问顶点最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-target-id": "请输入终点ID", - "input-integer": "请填写大于等于0的整数", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "select-property": "请选择属性", - "input-positive-integer": "请填写大于0的整数,默认为0", - "no-property": "无属性值为数字类型的属性", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "skip-degree": "当顶点的边数目大于填写值,则跳过当前顶点,用于规避超级点", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "integer-only": "请填写大于等于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数", - "postive-integer-only": "请填写大于0的整数" - } - }, - "single-source-weighted-shortest-path": { - "options": { - "source": "起点ID:", - "direction": "方向:", - "weight": "权重属性:", - "with_vertex": "返回顶点完整信息:", - "label": "边类型:", - "max_degree": "最大度数:", - "skip_degree": "跳过点的度数:", - "capacity": "遍历中访问顶点最大值:", - "limit": "返回顶点/最短路径最大值:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入起点ID", - "input-integer": "请填写大于等于0的整数,默认为0", - "input-positive-integer": "请填写大于0的整数", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-capacity": "请填写-1或大于0的整数,默认为10000000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10", - "no-property": "若不填写权重为1.0", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "skip-degree": "当顶点的边数目大于填写值,则跳过当前顶点,用于规避超级点", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "integer-only": "请填写大于等于0的整数", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - } - }, - "jaccard": { - "options": { - "vertex": "顶点1:", - "other": "顶点2:", - "direction": "方向:", - "label": "边类型:", - "max_degree": "最大度数:" - }, - "pre-value": "全部", - "placeholder": { - "input-source-id": "请输入顶点ID", - "input-other-id": "请输入不同于顶点1的ID", - "input-positive-integer": "请填写大于0的整数", - "input-positive-integer-or-negative-one-max-degree": "请填写-1或大于0的整数,默认为10000", - "no-edge-types": "无边类型" - }, - "hint": { - "max-depth": "为保证性能,建议不超过10步,推荐5步", - "max-degree": "查询过程中,单个顶点的最大边数目" - }, - "validations": { - "no-empty": "该项不能为空", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数", - "no-same-value-with-other": "不能与顶点2相同", - "no-same-value-with-vertex": "不能与顶点1相同" - } - }, - "personal-rank": { - "options": { - "source": "起点ID:", - "alpha": "Alpha:", - "max_depth": "迭代次数:", - "with_label": "返回结果筛选:", - "label": "边类型:", - "degree": "最大度数:", - "limit": "返回顶点最大值:", - "sorted": "返回结果排序:" - }, - "with-label-radio-value": { - "same_label": "相同类型顶点", - "other_label": "不同类型顶点", - "both_label": "全部类型顶点" - }, - "placeholder": { - "input-source-id": "请输入起点ID", - "input-positive-integer-or-negative-one-degree": "请填写-1或大于0的整数,默认为10000", - "input-positive-integer-or-negative-one-limit": "请填写-1或大于0的整数,默认为10000000", - "select-edge": "请选择边类型", - "input-positive-integer": "请填写大于0的整数", - "alpha": "请输入(0-1]的数字", - "max_depth": "请输入(0-50]的数字" - }, - "hint": { - "degree": "查询过程中,单个顶点的最大边数目", - "with-label": "根据是否与起点类型相同,筛选返回结果", - "sorted": "选择开,则降序排列,选择关,则不排序" - }, - "validations": { - "no-empty": "该项不能为空", - "no-edge-typs": "无边类型", - "alpha-range": "请填写大于0且小于等于1的数值", - "depth-range": "请填写大于0且小于等于50的数值", - "postive-integer-only": "请填写大于0的整数", - "positive-integer-or-negative-one-only": "请填写-1或大于0的整数" - }, - "pre-value": "全部", - "add-new-rule": "添加规则" - }, - "api-name-mapping": { - "rings": "环路检测", - "crosspoints": "交点检测", - "shortpath": "最短路径", - "allshortpath": "全最短路径", - "paths": "所有路径", - "fsimilarity": "模型相似度算法", - "neighborrank": "Neighbor Rank推荐算法", - "kneighbor": "k步邻居", - "kout": "k跳算法", - "customizedpaths": "自定义路径", - "rays": "射线检测", - "sameneighbors": "共同邻居", - "weightedshortpath": "带权最短路径", - "singleshortpath": "单源带权最短路径", - "jaccardsimilarity": "Jaccard相似度", - "personalrank": "Personal Rank推荐算法" - } - }, - "exec-logs": { - "table-title": { - "time": "时间", - "type": "执行类型", - "content": "执行内容", - "status": "状态", - "duration": "耗时", - "manipulation": "操作" - }, - "type": { - "GREMLIN": "GREMLIN 查询", - "GREMLIN_ASYNC": "GREMLIN 任务", - "ALGORITHM": "算法查询" - }, - "status": { - "success": "成功", - "async-success": "提交成功", - "running": "运行中", - "failed": "失败", - "async-failed": "提交失败" - } - } - } -} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/index.ts b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/index.ts deleted file mode 100644 index b3e03211e..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/graph-managment/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import CommonResources from './common.json'; -import GraphManagementSideBarResources from './GraphManagementSidebar.json'; -import DataAnalyzeResources from './dataAnalyze.json'; -import DataImportResources from './data-import/import-tasks/ImportTasks.json'; -import AsyncTasksResources from './AsyncTasks.json'; -import Addition from './addition.json'; - -export { - CommonResources, - DataAnalyzeResources, - GraphManagementSideBarResources, - DataImportResources, - AsyncTasksResources, - Addition -}; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/index.js b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/index.js new file mode 100644 index 000000000..4351084c8 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/index.js @@ -0,0 +1,46 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {merge} from 'lodash'; +import { + Board, + Common, + ERView, +} from './components'; +import { + Home, + Manage, + Analysis, + Modules, + Pages, +} from './modules'; + +const translation = { + translation: merge( + Board, + Common, + Home, + Manage, + ERView, + Analysis, + Modules, + Pages + ), +}; + +export default translation; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/index.ts b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/index.ts deleted file mode 100644 index 4a369f54e..000000000 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import { merge } from 'lodash-es'; -import { - CommonResources, - DataAnalyzeResources, - GraphManagementSideBarResources, - DataImportResources, - AsyncTasksResources, - Addition -} from './graph-managment'; - -const translation = { - translation: merge( - CommonResources, - DataAnalyzeResources, - GraphManagementSideBarResources, - DataImportResources, - AsyncTasksResources, - Addition - ) -}; - -export default translation; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json new file mode 100644 index 000000000..ab15ae4b4 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json @@ -0,0 +1,860 @@ +{ + "analysis": { + "name": "业务分析", + "query": { + "name": "图语言分析", + "placeholder": "请输入查询语句", + "gremlin_tab": "Gremlin 分析", + "cypher_tab": "Cypher 分析", + "text2gql_tab": "自然语言", + "text2gql_badge": "待实现", + "text2gql_title": "自然语言图查询", + "text2gql_description": "这是未来 Text2GQL 工作流的界面预览,尚未连接模型或查询服务。", + "text2gql_placeholder": "描述你希望查询的图问题", + "text2gql_privacy": "当前不会发送、生成、保存或执行任何内容。", + "execute_query": "执行查询", + "execute_task": "执行任务", + "execute_mode_desc": "查询模式适合 30 秒内可返回结果的小规模分析,任务模式适合较长时间返回结果的大规模分析,任务详情可在任务管理中查看", + "empty_query": "查询语句不能为空", + "collapse": "收起", + "expand": "展开", + "favorite": "收藏", + "favorite_statement": "收藏语句", + "favorite_name_placeholder": "请输入收藏名称", + "favorite_success": "收藏成功", + "favorite_failed": "收藏失败" + }, + "query_result": { + "graph": "图", + "table": "表格", + "no_data": "暂无数据结果", + "loading": "数据加载中...", + "run_failed": "运行失败", + "run_failed_action": "查询失败,请检查语句后重试。", + "submitting_task": "提交异步任务中...", + "submit_failed": "提交失败", + "import_failed": "导入失败", + "submit_success": "提交成功", + "no_graph_result": "无图结果,请查看表格或 Json 数据", + "no_table_result": "无表格结果,请查看图或 Json 数据", + "no_json_result": "无 Json 结果,请查看图或表格数据", + "expand_failed": "展开失败", + "no_more_neighbors": "不存在更多邻接点" + }, + "canvas": { + "import": "导入", + "export": "导出", + "style": "外观设置", + "filter": "筛选", + "layout": "布局方式", + "setting": "设置", + "new": "新建", + "statistics": "统计", + "mode_2d": "2D 模式", + "mode_3d": "3D 模式", + "import_success": "导入成功", + "export_json": "导出 Json", + "export_png": "导出图片", + "export_json_title": "导出 Json", + "export_image_title": "导出图片", + "file_name": "文件名称", + "file_name_required": "请输入文件名称", + "file_name_placeholder": "请输入文件名称", + "add_vertex": "新增顶点", + "add_in_edge": "新增入边", + "add_out_edge": "新增出边", + "toolbar": { + "clear_canvas": "清空画布", + "fit_center": "自适应", + "full_screen": "全屏", + "undo": "后退", + "redo": "前进", + "refresh_layout": "刷新布局", + "zoom_out": "缩小", + "zoom_in": "放大", + "fix_node": "固定", + "isolated_nodes": "孤立点" + }, + "clear_graph": { + "title": "是否清除当前画布", + "description": "清除当前画布,此次清除操作后不可以恢复", + "ok": "确认", + "cancel": "取消" + }, + "context_menu": { + "expand": "展开", + "search": "查询", + "fix": "固定", + "unfix": "取消固定", + "hide": "隐藏", + "add_out_edge": "添加出边", + "add_in_edge": "添加入边", + "add_vertex": "添加顶点" + }, + "tooltip": { + "import_2d": "当前图已有数据,请清空后再导入", + "import_3d": "3D 模式不支持导入", + "export_2d": "暂无可导出的图数据", + "export_3d": "3D 模式不支持导出", + "style_2d": "暂无可设置样式的图数据", + "style_3d": "3D 模式不支持外观设置", + "filter_2d": "暂无可筛选的图数据", + "filter_3d": "3D 模式不支持筛选", + "layout_2d": "暂无可布局的图数据", + "layout_3d": "3D 模式不支持布局方式", + "setting_2d": "暂无可配置的图数据", + "setting_3d": "3D 模式不支持设置", + "new_2d": "暂无可编辑的图数据", + "new_3d": "3D 模式不支持新建图元素", + "statistics_2d": "暂无可统计的图数据", + "statistics_3d": "3D 模式不支持统计", + "switch_2d": "暂无可切换的图数据" + }, + "canvas_3d": { + "node": "节点", + "edge": "边", + "type": "类型", + "id": "ID", + "tip": "提示:", + "left_mouse": "鼠标左键", + "right_mouse": "鼠标右键", + "rotate": "旋转", + "move": "移动", + "edge_label_type": { + "parent": "父边", + "sub": "子边" + } + }, + "filter_drawer": { + "title": "查询", + "filter": "筛选", + "logic": "逻辑表达式", + "filter_type": "筛选类型", + "operation_type": "操作类型", + "vertex": "点", + "edge": "边", + "vertex_property": "点属性", + "vertex_id": "点ID", + "vertex_label": "点类型", + "edge_property": "边属性", + "edge_id": "边ID", + "edge_label": "边类型", + "edge_direction": "边方向", + "property": "属性", + "rule": "规则", + "value": "值", + "add_expression": "添加表达式", + "delete_expression": "删除表达式", + "add_property": "添加属性", + "delete": "删除", + "duplicate_property": "属性不可重复", + "get_edge_failed": "获取边信息失败", + "get_adjacent_edge_failed": "获取邻边失败", + "number_placeholder": "请输入数字", + "string_placeholder": "请输入字符串", + "placeholder": "请输入", + "rule_option": { + "gt": "大于", + "gte": "大于等于", + "eq": "等于", + "lt": "小于", + "lte": "小于等于", + "true": "True", + "false": "False" + } + }, + "legend": { + "title": "图例" + }, + "element_tooltip": { + "node": "节点", + "edge": "边", + "type": "类型", + "edge_label_type": { + "parent": "父边", + "sub": "子边", + "normal": "普通边" + } + }, + "task_navigation": { + "success_alt": "提交成功", + "task_id": "任务ID", + "view": "查看" + }, + "setting_panel": { + "title": "设置", + "node_id": "节点ID", + "edge_type": "边类型", + "node_center": "节点中心", + "node_bottom": "节点底部", + "node_label": "节点标签", + "node_label_property": "节点标签属性名", + "node_label_position": "节点标签位置", + "node_label_size": "节点标签字体大小", + "edge_label": "边标签", + "edge_label_property": "边标签属性名", + "edge_label_size": "边标签字体大小", + "components": "组件", + "enable_minimap": "启用缩略图", + "enable_grid": "启用网格背景", + "enable_legend": "启用图例" + }, + "layout_panel": { + "title": "布局", + "enable_layout": "启用布局", + "enable_layout_tooltip": "是否启用布局,关闭后布局将切换为默认布局", + "layout_type": "布局方式", + "force": "力导布局", + "circular": "环形布局", + "concentric": "同心圆布局", + "dagre": "层次布局", + "grid": "网格布局", + "radial": "径向布局", + "node_size": "节点大小", + "node_size_tooltip": "节点大小(直径),用于碰撞检测。", + "link_distance": "边长度", + "node_strength": "点作用力", + "node_strength_tooltip": "节点作用力,正数代表节点之间的引力作用,负数代表节点之间的斥力作用", + "prevent_overlap": "是否防止重叠", + "node_spacing": "节点间距", + "node_spacing_tooltip": "preventOverlap开启时生效,防止重叠时节点边缘间距的最小值。", + "start_radius": "起始半径", + "start_radius_tooltip": "螺旋状布局的起始半径", + "end_radius": "结束半径", + "end_radius_tooltip": "螺旋状布局的结束半径", + "divisions": "分段数", + "divisions_tooltip": "节点在环上的分段数", + "angle_ratio": "角度比", + "angle_ratio_tooltip": "从第一个节点到最后一个节点相差多少个2 * PI", + "ordering": "排序依据", + "ordering_tooltip": "节点在环上排序的依据", + "ordering_data": "数据", + "ordering_topology": "拓扑", + "ordering_degree": "度数", + "clockwise": "是否顺时针排列", + "sweep": "弧度差", + "sweep_tooltip": "第一个节点与最后一个节点之间的弧度差。", + "start_angle": "起始弧度", + "start_angle_tooltip": "开始方式节点的弧度", + "equidistant": "环与环距离是否相等", + "rankdir": "布局方向", + "rankdir_tooltip": "布局的方向", + "align": "对齐", + "align_tooltip": "节点对齐方式", + "ranksep": "层间距", + "ranksep_tooltip": "层间距(px)。在rankdir 为 'TB' 或 'BT' 时是竖直方向相邻层间距;在rankdir 为 'LR' 或 'RL' 时代表水平方向相邻层间距", + "nodesep": "点间距", + "nodesep_tooltip": "节点间距(px)。在rankdir 为 'TB' 或 'BT' 时是节点的水平间距;在rankdir 为 'LR' 或 'RL' 时代表节点的竖直方向间距", + "top_bottom": "从上至下", + "bottom_top": "从下至上", + "left_right": "从左至右", + "right_left": "从右至左", + "align_center": "中间对齐", + "align_ul": "对齐到左上角", + "align_ur": "对齐到右上角", + "align_dl": "对齐到左下角", + "align_dr": "对齐到右下角", + "rows": "网格行数", + "rows_tooltip": "网格的行数,为 undefined 时算法会根据节点数量、布局空间、cols(若指定)自动计算", + "cols": "网格列数", + "cols_tooltip": "网格的列数,为 undefined 时算法根据节点数量、布局空间、rows(若指定)自动计算", + "unit_radius": "层级距离", + "unit_radius_tooltip": "每一圈距离上一圈的距离。默认填充整个画布,即根据图的大小决定", + "link_distance_short": "边长", + "focus_node": "中心节点", + "focus_node_tooltip": "辐射的中心点,默认为数据中第一个节点。可以传入节点 id 或节点本身", + "radial_node_spacing_tooltip": "preventOverlap 为 true 时生效, 防止重叠时节点边缘间距的最小值", + "radial_prevent_overlap_tooltip": "是否防止重叠,必须配合下面属性 nodeSize,只有设置了与当前图节点大小相同的 nodeSize 值,才能够进行节点重叠的碰撞检测", + "strict_radial": "严格辐射", + "strict_radial_tooltip": "是否必须是严格的 radial 布局" + }, + "statistics_panel": { + "label_statistics": "标签统计", + "graph_statistics": "图统计", + "type_count": "点边数量统计", + "type_count_desc": "当前画布中所有类型的点边和对应的点边数量", + "degree_top10": "节点权重Top10", + "degree_top10_desc": "当前画布中一度边数量最多的十个节点。", + "zero_degree_nodes": "孤立点", + "zero_degree_nodes_desc": "孤立点是指和其余点没有关联,在画布中独立存在的点。", + "zero_degree_edges": "孤立边", + "zero_degree_edges_desc": "只有一度关联的点的边。", + "incidence_nodes": "关联点", + "incidence_nodes_desc": "只有选中图上的一个点,才能显示相应的关联点的数量。", + "select_node": "请选择节点", + "highlight": "高亮", + "hide": "隐藏" + }, + "dynamic_add": { + "add_success": "添加成功", + "save_success": "保存成功", + "save_failed": "保存失败", + "get_edge_type_failed": "获取边类型失败", + "get_edge_property_failed": "获取边属性失败", + "get_vertex_property_failed": "获取点属性失败", + "nullable_property": "可空属性", + "non_nullable_property": "不可空属性", + "property": "属性", + "property_value": "属性值", + "property_placeholder": "请输入属性值", + "add_vertex": "添加顶点", + "add_in_edge": "添加入边", + "add_out_edge": "添加出边", + "add": "添加", + "cancel": "取消", + "vertex_type": "顶点类型", + "edge_type": "边类型", + "source": "起点", + "target": "终点", + "id_strategy": "ID策略", + "primary_key": "主键属性", + "id_value": "ID值", + "id_placeholder": "请输入ID值", + "select_vertex_type": "请选择顶点类型", + "select_edge_type": "请选择边类型", + "select_source": "请选择起点ID", + "select_target": "请选择终点ID", + "customize_string": "自定义字符串", + "primary_key_id": "主键ID", + "customize_number": "自定义数字", + "automatic": "自动生成", + "required": "此项不能为空" + }, + "edit_element": { + "save_success": "保存成功", + "save_failed": "保存失败", + "get_edge_property_failed": "获取边属性失败", + "get_vertex_property_failed": "获取点属性失败", + "type": "类型", + "edge_relation": "边类型关系", + "parent_edge": "父边", + "child_edge": "子边", + "id": "ID", + "source": "起点", + "target": "终点", + "primary_key": "主键", + "nullable_property": "可空属性", + "non_nullable_property": "不可空属性", + "property": "属性", + "property_value": "属性值", + "property_placeholder": "请输入属性值", + "edit_details": "编辑详情", + "data_details": "数据详情", + "save": "保存", + "edit": "编辑", + "close": "关闭", + "edge_type": "边类型", + "edge_id": "边ID", + "vertex_type": "顶点类型", + "vertex_id": "顶点ID", + "required": "此项不能为空", + "edge": "边", + "vertex": "顶点" + } + }, + "logs": { + "execute_tab": "执行记录", + "favorite_tab": "收藏的查询", + "favorite_success": "收藏成功", + "favorite_failed": "收藏失败", + "delete_success": "删除成功", + "delete_failed": "删除失败", + "edit_success": "修改成功", + "edit_failed": "修改失败", + "favorite_statement": "收藏语句", + "favorite_name_placeholder": "请输入收藏名称", + "edit_name": "修改名称", + "confirm_delete": "确认删除", + "delete_favorite_confirm": "是否确认删除该条收藏语句?", + "search_placeholder": "搜索收藏名称或语句", + "column": { + "time": "时间", + "type": "执行类型", + "content": "执行内容", + "status": "状态", + "duration": "耗时", + "action": "操作", + "name": "名称", + "favorite_statement": "收藏语句" + }, + "type": { + "GREMLIN": "GREMLIN 查询", + "GREMLIN_ASYNC": "GREMLIN 任务", + "ALGORITHM": "算法任务", + "CYPHER": "CYPHER 查询" + }, + "status": { + "SUCCESS": "成功", + "ASYNC_TASK_SUCCESS": "提交成功", + "ASYNC_TASK_RUNNING": "提交运行中", + "RUNNING": "运行中", + "FAILED": "失败", + "ASYNC_TASK_FAILED": "提交失败" + }, + "failure_reason": { + "GREMLIN_EXECUTION_FAILED": "查询失败,请检查语句后重试。" + }, + "action": { + "favorite": "收藏", + "load_statement": "加载语句", + "edit_name": "修改名称", + "save": "保存" + } + }, + "algorithm": { + "name": "图算法", + "placeholder": "算法查询", + "run": "运行", + "task_submit_success": "{{name}} 算法任务提交成功", + "common": { + "instance_num": "实例数", + "input_limit_edges_per_vertex": "最大出边限制", + "max_iter_step": "最大迭代次数", + "worker_num": "worker计算线程数", + "sample_rate": "边的采样率,由于此算法是指数型增长的算法,算力要求非常高,需要根据业务需求设置合理的采样率,得到一个近似结果", + "weight_property": "权重属性名", + "property_filter": "点边属性过滤条件", + "min_ring_length": "输出环路的最小长度", + "max_ring_length": "输出环路的最大长度", + "max_step": "最大迭代步数", + "request_memory": "计算节点最小内存需求", + "JVM_memory": "jvm环境内存大小,默认为32g", + "source": "起始点ID", + "query_tooltip": "仅当选择的图数据加载完成后,才可以使用OLAP算法。" + }, + "form": { + "vertices_required": "未指定 ids 或 label 和 properties 的联合查询", + "vertices_ids_tooltip": "通过顶点 id 列表提供起始或终止顶点,如果没有指定 ids,则使用 label 和 properties 的联合条件查询起始或终止顶点", + "vertices_label_tooltip": "顶点的类型,ids 参数为空时 label 和 properties 参数才会生效", + "vertices_properties_tooltip": "属性 Map,key 为属性名,value 为属性值,类型由 schema 定义决定。properties 中的属性值可以是列表,表示只要 key 对应的 value 在列表中就可以", + "direction_options": { + "out": "出边", + "in": "入边", + "both": "双边" + }, + "edge_steps": { + "label": "边类型", + "properties": "通过属性的值过滤边" + }, + "vertex_steps": { + "label": "点类型", + "properties": "通过属性的值过滤点" + }, + "step": { + "steps": "从起始顶点出发的 Step 集合", + "direction": "起始顶点向外发散的方向", + "labels": "边的类型列表", + "edge_properties": "属性 Map,通过属性的值过滤边,key 为属性名,value 为属性值,类型由 schema 定义决定。properties 中的属性值可以是列表,表示只要 key 对应的 value 在列表中就可以", + "max_depth": "步数", + "max_times": "当前 step 可以重复的次数,当为 N 时,表示从起始顶点可以经过当前 step 1-N 次", + "max_degree": "查询过程中,单个顶点遍历的最大邻接边数目", + "max_degree_compatible": "查询过程中,单个顶点遍历的最大邻接边数目。0.12 版之前 step 内仅支持 degree 作为参数名,0.12 开始统一使用 max_degree,并向下兼容 degree 写法", + "skip_degree": "用于设置查询过程中舍弃超级顶点的最小边数,即当某个顶点的邻接边数目大于 skip_degree 时,完全舍弃该顶点。选填项,开启时需满足 skip_degree >= max_degree,默认为 0 表示不跳过任何点。开启后会有额外遍历开销,请确认理解后再开启", + "edge_steps": "边 Step 集合", + "vertex_steps": "点 Step 集合" + } + }, + "validation": { + "integer": "请输入整数", + "range_or_minus_one": "请输入 -1 或大于 0 的整数", + "max_depth_range": "请输入 (0, 5000] 范围的整数", + "max_limit_range": "请输入 (0, 800000) 范围的整数", + "non_negative_integer": "请输入大于等于 0 的整数", + "positive_integer": "请输入大于 0 的整数", + "skip_degree_range": "请输入 [0, 10000000] 的整数", + "group_property_range": "请输入大于 2 的整数", + "alpha_range": "请输入 (0, 1] 的数", + "top_range": "请输入 (0, 1000) 范围的数", + "open_unit_range": "请输入 (0, 1) 范围的数", + "depth_2_50": "请输入 [2, 50] 的整数", + "properties_format": "请按照 key=value 的格式换行输入", + "string_value_quote": "字符串 value 请用单引号 '' 包围", + "open_unit_value": "请输入 (0, 1) 范围的值", + "closed_unit_value": "请输入 [0, 1] 范围的值", + "integer_1_2000": "请输入 1 到 2000 范围的整数", + "value_1_100000": "请输入 1 到 100000 范围的值", + "integer_0_100000": "请输入 [0, 100000] 范围的整数", + "integer_1_10000": "请输入 1 到 10000 之间的整数值" + }, + "result": { + "similarity_value": "相似度的值", + "rank_score": "排名得分:", + "category": "分类{{index}}", + "no_neighbor_at_degree": "本场景下没有第{{index}}度邻居" + }, + "mode": { + "OLTP": "OLTP算法", + "OLAP": "OLAP算法" + }, + "capacity_item": { + "tooltip": "遍历过程中最大的访问的顶点数目" + }, + "direction_item": { + "tooltip": "顶点向外发散的方向" + }, + "label_item": { + "tooltip": "边的类型(默认代表所有edge label)" + }, + "max_degree_item": { + "tooltip": "查询过程中,单个顶点遍历的最大邻接边数目" + }, + "max_depth_item": { + "tooltip": "步数" + }, + "nearest_item": { + "tooltip": "nearest为true时,代表起始顶点到达结果顶点的最短路径长度为depth,不存在更短的路径;near\n est为false时,代表起始顶点到结果顶点有一条长度为depth的路径(未必最短且可以有环)", + "placeholder": "最短路径长度" + }, + "olap": { + "betweenness_centrality": { + "desc": "中介中心性算法(Betweenness Centrality)判断一个节点具有\"桥梁\"节点的值, 值越大说明它作为图中两点间必经路径的可能性越大, 典型的例子包括社交网络中的共同关注的人", + "sample_rate": "边的采样率", + "sample_rate_long": "边的采样率,由于此算法是指数型增长的算法,算力要求非常高,需要根据业务需求设置合理的采样率,得到一个近似结果", + "use_endpoint": "是否计算最后一个点" + }, + "closeness_centrality": { + "desc": "计算一个节点到所有其他可达节点的最短距离的倒数,进行累积后归一化的值。用于计算图中每个节点的度中心性值,支持无向图和有向图。", + "weight_property": "权重属性名", + "sample_rate": "边的采样率", + "wf_improved": "是否使用 Wasserman and Faust 紧密中心性公式" + }, + "cluster_coefficient": { + "desc": "聚集系数,计算每个点局部的聚集系数, 暂时未提供全局聚集系数。", + "minimum_edges_use_superedge_cache": "利用内存减少消息量,如果内存不够,可以从100改成1000,但聚集系数可能计算不完" + }, + "degree_centrality": { + "desc": "用于计算图中每个节点的度中心性值,支持无向图和有向图。", + "direction": "方向,in/out/both 入边/出边/双边" + }, + "filtered_rings_detection": { + "desc": "带过滤条件的环路检测算法(Filtered Rings Detection)用于检测图中的环路,\n 环路的路径由环路中最小id的顶点来记录。可通过指定点、边属性过滤规则让算法选择性的做路径传播。" + }, + "filter_subgraph_matching": { + "desc": "带属性过滤的子图匹配算法。用户可以传入一个带属性过滤的查询图结构,算法会在图中匹配所有与该查询图同构的子图。", + "query_graph_config": "查询图配置,json数组字符串" + }, + "k_core": { + "desc": "K-Core算法,标记所有度数为K的顶点。", + "k": "K-Core算法的k值,非必需,有默认值", + "degree_k": "最小度数阈值" + }, + "label_propagation_algorithm": { + "desc": "标签传递算法,是一种图聚类算法,常用在社交网络中。" + }, + "links": { + "desc": "链路追踪算法,通过指定的一批开始顶点,按照指定的传播规则进行传播,到指定的结束条件后停止并记录下路径。", + "analyze_config": "链路传播条件配置" + }, + "louvain": { + "desc": "Louvain 算法是基于模块度的社区发现算法。由于Louvain算法的特殊性,只用一个worker instance运行。" + }, + "computer_item": { + "computer_cpu": "master最大CPU", + "worker_cpu": "worker最大CPU", + "master_request_memory": "master最小内存,不满足最小内存则分配不成功", + "worker_request_memory": "worker最小内存,不满足最小内存则分配不成功", + "master_memory": "master最大内存,超过最大内存则会被k8s中止", + "worker_memory": "worker最大内存,超过最大内存则会被k8s中止" + }, + "item": { + "PAGE_RANK": "PageRank", + "WEAKLY_CONNECTED_COMPONENT": "Weakly Connected Component", + "DEGREE_CENTRALIT": "Degree Centrality", + "CLOSENESS_CENTRALITY": "Closeness Centrality", + "TRIANGLE_COUNT": "Triangle Count", + "K_NEIGHBOR": "K-neighbor(GET,基础版)", + "K_OUT": "K-out API(GET,基础版)", + "SAME_NEIGHBORS": "Same Neighbors", + "RINGS": "Rings", + "SHORTEST_PATH": "Shortest Path", + "ALLPATHS": "查找所有路径(POST,高级版)", + "JACCARD_SIMILARITY": "Jaccard Similarity(GET)", + "CROSSPOINTS": "Crosspoints", + "RINGS_DETECTION": "Rings Detection", + "FILTERED_RINGS_DETECTION": "Filtered Rings Detection", + "LINKS": "Links", + "CLUSTER_COEFFICIENT": "Cluster Coefficient", + "BETWEENNESS_CENTRALITY": "Betweenness Centrality", + "LABEL_PROPAGATION_ALGORITHM": "Label Propagation Algorithm", + "LOUVAIN": "Louvain", + "FILTER_SUBGRAPH_MATCHING": "Filter SubGraph Matching", + "K_CORE": "K-Core", + "PERSONAL_PAGE_RANK": "PersonalPageRank", + "KOUT_POST": "K-out API(POST,高级版)", + "KNEIGHBOR_POST": "K-neighbor API(POST,高级版)", + "JACCARD_SIMILARITY_POST": "Jaccard Similarity(POST)", + "RANK_API": "rank API", + "NEIGHBOR_RANK_API": "Neighbor Rank API", + "FINDSHORTESTPATH": "查找最短路径", + "FINDSHORTESTPATHWITHWEIGHT": "查找带权重的最短路径", + "SINGLESOURCESHORTESTPATH": "(从一个顶点出发)查找最短路径", + "MULTINODESSHORTESTPATH": "(指定顶点集)查找最短路径", + "CUSTOMIZEDPATHS": "自定义路径查询", + "TEMPLATEPATHS": "模版路径查询", + "CUSTOMIZED_CROSSPOINTS": "Customized Crosspoints", + "RAYS": "Rays", + "PATHS": "查找所有路径(GET,基础版)", + "FUSIFORM_SIMILARITY": "Fusiform Similarity", + "ADAMIC_ADAR": "Adamic Adar", + "RESOURCE_ALLOCATION": "Resource Allocation", + "SAME_NEIGHBORS_BATCH": "Same Neighbors Batch", + "EGONET": "Egonet", + "SSSP": "SSSP(单元最短路径)" + }, + "legacy_item_alias": { + "KOUT_POST": "K-out API(POST, 高级版)" + }, + "page_rank": { + "desc": "PageRank算法又称网页排名算法,是一种由搜索引擎根据网页(节点)之间相互的超链接进行计算的技术,用来体现网页(节点)的相关性和重要性。", + "alpha": "权重系数(又称阻尼系数)", + "l1": "收敛精度,为每次迭代各个点相较于上次迭代变化的绝对值累加和上限,当小于这个值时认为计算收敛,算法停止。", + "damping": "阻尼系数,传导到下个点的百分比", + "diff": "收敛精度,为每次迭代各个点相较于上次迭代变化的绝对值累加和上限,当小于这个值时认为计算收敛,算法停止。" + }, + "personal_page_bank": { + "desc": "PersonalPageRank 算法又称个性化推荐算法,是一种由搜索引擎根据网页(节点)之间相互的超链接进行计算的技术,用来体现网页(节点)的相关性和重要性", + "source": "起始顶点", + "alpha": "权重系数(又称阻尼系数)", + "l1": "收敛精度", + "use_id_fixlength": "true时,系统采用自增id运算", + "use_id_fixlength_query": "是否采用自增id运算" + }, + "ring_detection": { + "desc": "环路检测算法(Rings Detection),用于检测图中的环路,环路的路径由环路中最小id的顶点来记录。" + }, + "SSSPVermeer": { + "desc": "单元最短路径算法,求一个点到其他所有点的最短距离" + }, + "triangle_count": { + "desc": "三角形计数算法,用于计算通过每个顶点的三角形个数。", + "limit_edges_in_one_vertex": "最大出边限制", + "minimum_edges_use_superedge_cache": "利用内存减少消息量,如果内存不够,可以从100改成1000,但三角计数可能计算不完" + }, + "weakly_connected_component": { + "desc": "弱连通分量,计算无向图中所有联通的子图,输出各顶点所属的弱联通子图id" + } + }, + "oltp": { + "common": { + "source_vertex_id": "起始顶点 id", + "target_vertex_id": "目的顶点 id", + "vertex_id": "顶点 id", + "other_vertex_id": "另一个顶点 id", + "direction_source_target": "起始顶点到目的顶点的方向,目的点到起始点是反方向,BOTH 时不考虑方向", + "limit_crosspoints": "返回的交点的最大数目", + "max_steps": "最大步数", + "weight_property": "边的权重属性,属性类型必须为数字类型;如果不填,或边没有该属性,则权重为 1.0", + "alpha": "每轮迭代时从某个点往外走的概率,与 PageRank 算法中的 alpha 类似", + "iterations": "迭代次数", + "limit_vertices": "返回顶点的最大数目", + "source_target_path": "表示从起始顶点到终止顶点走过的路径" + }, + "template_paths": { + "desc": "根据一批起始顶点、边规则(包括方向、边的类型和属性过滤)和最大深度等条件查找符合条件的所有路径", + "sources": "起始顶点", + "targets": "终止顶点", + "with_ring": "true 表示包含环路;false 表示不包含环路", + "capacity": "遍历过程中最大的访问的顶点数目", + "limit": "返回的路径的最大条数" + }, + "kout_get": { + "desc": "根据起始顶点、方向、边的类型(可选)和深度 depth,查找从起始顶点出发恰好 depth 步可达的顶点" + }, + "kneighbor_get": { + "desc": "根据起始顶点、方向、边的类型(可选)和深度 depth,查找包括起始顶点在内、depth 步之内可达的所有顶点" + }, + "kout_post": { + "desc": "根据起始顶点、方向、边的类型(可选)和深度 depth,查找从起始顶点出发恰好 depth 步可达的顶点", + "source": "起始顶点 id", + "capacity": "遍历过程中最大的访问的顶点数目", + "limit": "返回的顶点的最大数目", + "algorithm": "遍历方式。通常 deep_first(深度优先搜索)方式会有更好的遍历性能。但当 nearest 为 true 时,可能会包含非最近邻节点,尤其在数据量较大时", + "algorithm_placeholder": "选择遍历方式", + "breadth_first": "广度优先", + "deep_first": "深度优先" + }, + "same_neighbors": { + "desc": "查询两个点的共同邻居", + "limit": "返回的共同邻居的最大数目" + }, + "rings": { + "desc": "根据起始顶点、方向、边的类型(可选)和最大深度等条件查找可达的环路", + "source_in_ring": "环路是否包含起点" + }, + "shortest_path": { + "desc": "根据起始顶点、目的顶点、方向、边的类型(可选)和最大深度,查找一条最短路径" + }, + "jaccard_similarity_get": { + "desc": "计算两个顶点的 Jaccard similarity,即两个顶点邻居的交集比上两个顶点邻居的并集" + }, + "crosspoints": { + "desc": "根据起始顶点、目的顶点、方向、边的类型(可选)和最大深度等条件查找相交点" + }, + "find_shortest_path": { + "desc": "根据起始顶点、目的顶点、方向、边的类型(可选)和最大深度,查找两点间所有的最短路径" + }, + "find_shortest_path_with_weight": { + "desc": "根据起始顶点、目的顶点、方向、边的类型(可选)和最大深度,查找一条带权最短路径" + }, + "paths": { + "desc": "根据起始顶点、目的顶点、方向、边的类型(可选)和最大深度等条件查找所有路径", + "limit": "查询到的目标顶点个数,也是返回的最短路径的条数" + }, + "rays": { + "desc": "根据起始顶点、方向、边的类型(可选)和最大深度等条件查找发散到边界顶点的路径" + }, + "rank_api": { + "desc": "根据某个点现有的出边,推荐具有相近或相同关系的其他点", + "label": "源点出发的某类边 label,须连接两类不同顶点", + "max_diff": "提前收敛的精度差", + "sorted": "返回的结果是否根据 rank 排序", + "with_label": "筛选结果中保留哪些结果,可从 SAME_LABEL、OTHER_LABEL、BOTH_LABEL 中选择", + "same_label": "仅保留与源顶点相同类别的顶点", + "other_label": "仅保留与源顶点不同类别(二分图的另一端)的顶点", + "both_label": "同时保留与源顶点相同和相反类别的顶点" + }, + "neighbor_rank_api": { + "desc": "在一般图结构中,找出每一层与给定起点相关性最高的前 N 个顶点及其相关度", + "top": "在结果中每一层只保留权重最高的前 N 个结果" + }, + "jaccard_similarity_post": { + "desc": "计算与指定顶点的 Jaccard similarity 最大的 N 个点", + "top": "返回一个起点的 Jaccard similarity 中最大的 top 个" + }, + "kneighbor_post": { + "desc": "根据起始顶点、步骤(包括方向、边类型和过滤属性)和 depth,查找从起始顶点出发 depth 步内可达的所有顶点" + }, + "single_source_shortest_path": { + "desc": "从一个顶点出发,查找该点到图中其他顶点的最短路径(可选是否带权重)" + }, + "multi_nodes_shortest_path": { + "desc": "查找指定顶点集两两之间的最短路径", + "vertices": "起始顶点" + }, + "all_paths": { + "desc": "根据起始顶点、目的顶点、步骤(step)和最大深度等条件查找所有路径" + }, + "fusiform_similarity": { + "desc": "按照条件查询一批顶点对应的“梭形相似点”", + "min_neighbors": "最少邻居数目。邻居数目少于这个阈值时,认为起点不具备“梭形相似点”。", + "alpha": "相似度,代表起点与梭形相似点的共同邻居数目占起点全部邻居数目的比例", + "min_similars": "“梭形相似点”的最少个数。只有当起点的“梭形相似点”数目大于或等于该值时,才会返回起点及其“梭形相似点”", + "top": "返回一个起点的“梭形相似点”中相似度最高的 top 个,0 表示全部", + "group_property": "与 min_groups 一起使用。当起点跟其所有“梭形相似点”的某个属性值有至少 min_groups 个不同值时,才会返回该起点及其“梭形相似点”;不填代表不根据属性过滤", + "min_groups": "与 group_property 一起使用,只有设置 group_property 时才有意义", + "limit": "返回的结果数目上限。一个起点及其“梭形相似点”算一个结果", + "with_intermediary": "是否返回起点及其“梭形相似点”共同关联的中间点" + }, + "resource_allocation": { + "desc": "主要用于社交网络中判断两点紧密度的算法,用来求两点间共同邻居密集度的一个系数" + }, + "same_neighbors_batch": { + "desc": "批量查询两个点的共同邻居", + "vertex_list": "点 ID 对列表,如:[[\"0000000001\",\"0000376440\"],[\"0000000001\",\"0001822679\"]]" + }, + "egonet": { + "desc": "查找一批顶点的 K 层邻居,并支持针对不同点 / 边 Label 的过滤条件,结果中会包含起始顶点", + "sources": "起始顶点 id 集合,支持传入多个不同顶点" + }, + "customized_paths": { + "desc": "根据一批起始顶点、边规则(包括方向、边的类型和属性过滤)和最大深度等条件查找符合条件的所有路径", + "sort_by": "根据路径的权重排序:NONE 表示不排序,INCR 表示按照路径权重升序排序,DECR 表示按照路径权重降序排序", + "sort_none": "不排序", + "sort_incr": "按照路径权重升序", + "sort_decr": "按照路径权重降序", + "weight_by": "根据指定的属性计算边的权重,sort_by 不为 NONE 时有效,与 default_weight 互斥", + "default_weight": "当边没有属性作为权重计算值时采取的默认权重,sort_by 不为 NONE 时有效,与 weight_by 互斥", + "sample": "当需要对某个 step 的符合条件的边进行采样时设置,-1 表示不采样" + }, + "customized_crosspoints": { + "desc": "根据一批起始顶点、多种边规则(包括方向、边的类型和属性过滤)和最大深度等条件查找符合条件的所有路径终点的交集", + "path_patterns": "表示从起始顶点走过的路径规则,是一组规则的列表", + "limit": "返回的路径的最大数目" + }, + "adamic_adar": { + "desc": "主要用于社交网络中判断两点紧密度的算法, 用来求两点间共同邻居密集度的一个系数", + "vertex": "起始顶点", + "other": "终点顶点", + "direction": "起始顶点到目的顶点的方向,目的地到起始点是反方向,BOTH时不考虑方向", + "label": "默认代表所有edge label", + "max_degree": "查询过程中,单个顶点遍历的最大邻接边数目", + "select_direction": "顶点向外发散的方向" + } + } + }, + "async_task": { + "name": "任务管理", + "search_placeholder": "请输入任务ID或名称", + "get_failed": "获取异步任务失败", + "delete_failed": "删除失败", + "abort_failed": "终止失败", + "delete_confirm_title": "删除任务", + "delete_confirm_content": "确认删除该任务?删除后无法恢复,请谨慎操作", + "batch_delete_title": "批量删除", + "batch_delete_content": "确认删除以下任务?删除后无法恢复,请谨慎操作", + "selected_count": "已选{{count}}项", + "batch_delete": "批量删除", + "column": { + "task_id": "任务ID", + "task_name": "任务名称", + "task_type": "任务类型", + "create_time": "创建时间", + "duration": "耗时", + "status": "状态", + "action": "操作" + }, + "type": { + "all": "全部", + "gremlin": "Gremlin任务", + "algorithm": "算法任务", + "remove_schema": "删除元数据", + "create_index": "创建索引", + "rebuild_index": "重建索引", + "cypher": "Cypher任务", + "vermeer_load": "vermeer图加载任务", + "vermeer_compute": "vermeer图计算任务" + }, + "status": { + "all": "全部", + "unknown": "未知", + "new": "初始化", + "scheduling": "调度中", + "scheduled": "已调度", + "queued": "排队中", + "running": "运行中", + "restoring": "恢复中", + "success": "成功", + "failed": "失败", + "cancelled": "已取消", + "cancelling": "取消中", + "hanging": "挂起", + "pending": "挂起", + "deleting": "删除中" + }, + "action": { + "abort": "终止", + "aborting": "终止中", + "delete": "删除", + "check_result": "查看结果", + "check_reason": "查看原因" + } + }, + "topbar": { + "current_graph_space": "当前图空间:", + "current_graph": "当前图:", + "select": "请选择", + "loaded": "已加载", + "loading": "加载中", + "load_failed": "加载失败", + "not_loaded": "未加载", + "recent_load_time": "最近加载时间:", + "load_to_vermeer": "加载到Vermeer", + "reload_to_vermeer": "重加载到Vermeer", + "metadata_config": "元数据配置", + "metadata_tooltip": "点击可跳转到对应图的元数据配置的页面", + "olap_result": "是否查询OLAP结果:", + "get_vermeer_failed": "获取vermeer状态失败", + "load_vermeer_failed": "加载Vermeer任务失败" + } + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/home.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/home.json new file mode 100644 index 000000000..3dcccac64 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/home.json @@ -0,0 +1,10 @@ +{ + "home": { + "navigation": "导航", + "name":"系统管理" , + "my": "个人中心", + "account": "账号管理", + "resource": "资源管理", + "role": "角色管理" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/index.js b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/index.js new file mode 100644 index 000000000..9155cab6f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/index.js @@ -0,0 +1,30 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import Home from './home.json'; +import Manage from './manage.json'; +import Analysis from './analysis.json'; +import Modules from './modules.json'; +import Pages from './pages.json'; +export { + Home, + Analysis, + Manage, + Modules, + Pages, +}; diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/manage.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/manage.json new file mode 100644 index 000000000..9ec249de0 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/manage.json @@ -0,0 +1,8 @@ +{ + "manage": { + "name": "数据管理", + "graphspace": "图管理", + "source": "数据源管理", + "task": "数据导入" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/modules.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/modules.json new file mode 100644 index 000000000..617729b18 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/modules.json @@ -0,0 +1,290 @@ +{ + "topbar": { + "current_graphspace": "当前图空间:", + "current_graph": "当前图:", + "graph_select_placeholder": "请选择", + "status": { + "loaded": "已加载", + "loading": "加载中", + "error": "加载失败", + "created": "未加载" + }, + "last_load_time": "最近加载时间:", + "reload_to_vermeer": "重加载到Vermeer", + "load_to_vermeer": "加载到Vermeer", + "meta_config": "元数据配置", + "meta_config_tooltip": "点击可跳转到对应图的元数据配置的页面", + "olap_switch": "是否查询OLAP结果:", + "olap_on": "开启", + "olap_off": "关闭", + "load_fail": "加载Vermeer任务失败" + }, + "execute_log": { + "col": { + "time": "时间", + "type": "执行类型", + "content": "执行内容", + "status": "状态", + "duration": "耗时", + "operation": "操作" + }, + "type": { + "GREMLIN": "GREMLIN查询", + "GREMLIN_ASYNC": "GREMLIN任务", + "ALGORITHM": "算法任务", + "CYPHER": "CYPHER查询" + }, + "status": { + "SUCCESS": "成功", + "ASYNC_TASK_SUCCESS": "提交成功", + "ASYNC_TASK_RUNNING": "提交运行中", + "RUNNING": "运行中", + "FAILED": "失败", + "ASYNC_TASK_FAILED": "提交失败" + }, + "favorite": "收藏", + "load_statement": "加载语句", + "favorite_title": "收藏语句", + "favorite_placeholder": "请输入收藏名称" + }, + "favorite": { + "col": { + "name": "收藏名称", + "content": "执行内容", + "create_time": "收藏时间", + "operation": "操作" + }, + "edit_title": "修改名称", + "edit_placeholder": "请输入收藏名称", + "load_statement": "加载语句", + "delete_confirm": "确定删除?", + "search_placeholder": "搜索收藏名称" + }, + "query_bar": { + "tab_execute": "执行记录", + "tab_favorite": "收藏", + "gremlin_placeholder": "请输入查询语句", + "execute": "执行", + "async_execute": "异步执行", + "clear": "清空" + }, + "query_result": { + "tab_graph": "图结果", + "tab_table": "表格结果", + "tab_json": "JSON结果", + "empty": "无结果", + "vertex_count": "顶点数: {{count}}", + "edge_count": "边数: {{count}}" + }, + "async_task": { + "col": { + "id": "任务ID", + "type": "任务类型", + "status": "状态", + "create_time": "创建时间", + "update_time": "更新时间", + "operation": "操作" + }, + "detail": { + "title": "异步任务", + "id": "任务ID", + "type": "类型", + "status": "状态", + "create_time": "创建时间", + "update_time": "更新时间", + "content": "执行内容", + "result": "执行结果" + }, + "cancel": "取消任务", + "cancel_confirm": "确定取消此任务?" + }, + "graph_component": { + "search": { + "placeholder": "请输入顶点ID或属性值", + "search_btn": "搜索", + "vertex_id": "顶点ID", + "property": "属性", + "result_count": "找到 {{count}} 个结果" + }, + "edit_element": { + "title_vertex": "编辑顶点", + "title_edge": "编辑边", + "id": "ID", + "label": "类型", + "properties": "属性", + "save": "保存", + "cancel": "取消", + "delete": "删除" + }, + "export": { + "title": "导出数据", + "json": "导出JSON", + "csv": "导出CSV", + "image": "导出图片" + }, + "menu": { + "expand": "展开", + "hide": "隐藏", + "show_properties": "显示属性", + "hide_properties": "隐藏属性", + "fix": "固定", + "unfix": "取消固定" + }, + "status": { + "vertex_count": "顶点: {{count}}", + "edge_count": "边: {{count}}" + }, + "clear": "清空图", + "clear_confirm": "确定清空图?", + "fit_center": "适配画布", + "fix_node": "固定节点", + "full_screen": "全屏", + "zoom_in": "放大", + "zoom_out": "缩小", + "redo": "重做", + "undo": "撤销", + "refresh": "刷新", + "zero_degree": "显示孤立节点", + "legend": "图例", + "3d_mode": "3D模式", + "2d_mode": "2D模式" + }, + "filter": { + "title": "过滤器", + "add": "添加过滤条件", + "vertex_filter": "顶点过滤", + "edge_filter": "边过滤", + "property": "属性", + "operator": "操作符", + "value": "值", + "apply": "应用", + "reset": "重置", + "and": "且", + "or": "或" + }, + "layout": { + "title": "布局配置", + "type": "布局类型", + "force": "力导向", + "circular": "环形", + "radial": "放射", + "dagre": "层次", + "grid": "网格", + "concentric": "同心圆", + "apply": "应用", + "node_size": "节点大小", + "link_distance": "边距离", + "node_strength": "节点强度", + "edge_strength": "边强度", + "gravity": "引力", + "speed": "速度", + "iterations": "迭代次数", + "radius_ratio": "半径比例", + "node_sep": "节点间距", + "rank_sep": "层间距", + "direction": "方向", + "LR": "左到右", + "RL": "右到左", + "TB": "上到下", + "BT": "下到上", + "start_radius": "起始半径", + "end_radius": "终止半径", + "sweep": "旋转角度", + "equal_node_spacing": "等间距", + "min_node_spacing": "最小间距", + "start_angle": "起始角度", + "clockwise": "顺时针", + "prevent_overlap": "防止重叠", + "node_spacing": "节点间距", + "cols": "列数", + "rows": "行数" + }, + "style_config": { + "title": "样式配置", + "color_picker": "选择颜色", + "close_color_picker": "关闭颜色选择器", + "vertex_style": "顶点样式", + "edge_style": "边样式", + "vertex_appearance": "点外观", + "edge_appearance": "边外观", + "basic_properties": "基础属性", + "appearance_type": "点外观 / 边外观", + "appearance_config": "外观配置", + "select_placeholder": "请选择", + "type": "类型", + "shape": "形状", + "shape_circle": "圆型", + "shape_diamond": "菱形", + "shape_triangle": "三角形", + "shape_star": "五角星", + "shape_ellipse": "椭圆", + "border_color": "边框色", + "border_width": "边框粗细", + "fill_color": "填充色", + "icon_style": "图标样式", + "icon_color": "图标颜色", + "label_size": "标签大小", + "label_color": "标签颜色", + "node_opacity": "节点透明度", + "edge_width": "边的粗细", + "edge_color": "边的颜色", + "edge_animation": "边动画", + "line_solid": "直线", + "line_dashed": "虚线", + "color": "颜色", + "size": "大小", + "label": "标签", + "icon": "图标", + "line_type": "线型", + "line_width": "线宽", + "arrow": "箭头", + "display_field": "展示字段", + "apply": "应用", + "reset": "重置" + }, + "statistics": { + "title": "图统计", + "vertex_count": "顶点数", + "edge_count": "边数", + "vertex_types": "顶点类型", + "edge_types": "边类型", + "loading": "加载中..." + }, + "dynamic_add": { + "vertex": { + "title": "新增顶点", + "id": "顶点ID", + "type": "顶点类型", + "properties": "属性", + "submit": "提交", + "cancel": "取消" + }, + "edge": { + "title": "新增边", + "type": "边类型", + "source": "起点", + "target": "终点", + "properties": "属性", + "submit": "提交", + "cancel": "取消" + } + }, + "navigation_module": { + "create_data": { + "title": "创建数据", + "graph_management": "图管理", + "meta_management": "元数据管理", + "data_import": "数据导入" + }, + "analysis": { + "title": "业务分析", + "graph_query": "图查询", + "algorithm": "图算法" + }, + "system": { + "title": "系统管理", + "user_management": "用户管理", + "role_management": "角色管理" + } + } +} diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json new file mode 100644 index 000000000..ab4aa73b1 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json @@ -0,0 +1,833 @@ +{ + "common": { + "add_success": "添加成功", + "batch_delete": "批量删除", + "create": "创建", + "delete": "删除", + "delete_success": "删除成功", + "edit": "编辑", + "no": "否", + "operation": "操作", + "refresh": "刷新", + "select_at_least_one": "请至少选择一项", + "update_success": "更新成功", + "yes": "是", + "action": { + "create": "创建", + "save": "保存", + "edit": "编辑", + "delete": "删除", + "cancel": "取消", + "confirm": "确认", + "add": "添加", + "search": "搜索", + "reset": "重置", + "view": "查看", + "detail": "详情", + "upload": "上传", + "download": "下载", + "refresh": "刷新", + "submit": "提交", + "clear": "清空", + "next": "下一步", + "back": "返回", + "close": "关闭", + "apply": "应用", + "assign_permission": "分配权限", + "set_default": "设为默认", + "schema_manage": "schema管理", + "init": "初始化", + "clone": "克隆" + }, + "label": { + "default": "默认", + "unlimited": "未限制", + "unknown": "未知", + "warning": "警告", + "yes": "是", + "no": "否", + "view_mode": "图模式", + "list_mode": "列表模式", + "loading": "加载中..." + }, + "verify": { + "yes": "是", + "no": "否" + }, + "msg": { + "success": "操作成功", + "create_success": "创建成功", + "update_success": "更新成功", + "delete_success": "删除成功", + "set_success": "设置成功", + "init_success": "初始化成功", + "upload_success": "上传成功", + "upload_fail": "上传失败", + "enable_success": "启动成功", + "enable_fail": "启动失败", + "disable_success": "暂停成功", + "disable_fail": "暂停失败", + "delete_fail": "删除失败", + "operation_failed": "操作失败,请检查服务连接后重试。", + "load_failed": "数据加载失败,请检查服务连接后重试。", + "select_one": "请至少选择一项" + }, + "confirm": { + "delete": "确定删除?", + "delete_irrecoverable": "删除后不可恢复" + }, + "form": { + "required": "此项为必填项", + "max_12": "字符长度最多12位", + "max_48": "字符长度最多48位", + "max_128": "最多字符128位" + } + }, + "graphspace": { + "title": "图空间管理", + "load": { + "unavailable": "无法加载图空间列表。", + "retry": "重试图空间列表" + }, + "create": "创建图空间", + "search_placeholder": "请输入图空间名称", + "set_default_confirm": "确认更改图空间的默认设置?", + "delete_confirm": "确定删除?", + "delete_content": "删除空间后不可恢复", + "col": { + "name": "名称", + "auth": "是否开启鉴权", + "description": "描述", + "graph_service": "图服务", + "compute_task": "计算任务", + "storage_limit": "最大存储空间限制", + "operation": "操作" + }, + "auth_yes": "是", + "auth_no": "否", + "unit": { + "cpu": "核", + "memory": "G", + "unlimited": "未限制" + }, + "form": { + "title_edit": "编辑图空间", + "title_create": "创建图空间", + "id": "图空间ID", + "name": "图空间名称", + "auth": "是否开启鉴权", + "max_graph": "最大图数", + "max_role": "最大角色数", + "graph_service": "图服务:", + "compute_task": "计算任务:", + "storage": "存储服务:", + "cpu": "cpu资源:", + "memory": "内存资源:", + "k8s_namespace": "k8s命名空间", + "operator_image": "Operator镜像地址", + "algorithm_image": "算法镜像地址", + "storage_limit": "最大存储空间限制:", + "admin": "图空间管理员", + "admin_placeholder": "选择图空间管理员", + "description": "描述", + "description_placeholder": "图空间描述,可选", + "cpu_placeholder": "核", + "memory_placeholder": "G", + "id_placeholder": "以字母开头,只能包含小写字母、数字、_, 最长48位", + "name_placeholder": "只能包含中文、字母、数字、_,最长12位", + "k8s_placeholder": "以字母开头,只能包含小写字母、数字、-,最长48位", + "image_format_error": "请输入正确的镜像地址格式(ie: example.com/org_1/xx_img:1.0.0)!", + "id_rule": "以字母开头,只能包含小写字母、数字、_, 最长48位", + "k8s_rule": "以字母开头,只能包含小写字母、数字、-,最长48位" + }, + "card": { + "enter": "进入图空间", + "vertex": "顶点", + "edge": "边", + "daily_update": "点边数每日更新", + "last_update": "本次数据更新于 {{date}}", + "graph_id": "图ID:{{name}}", + "auth_label": "是否鉴权:", + "max_graph": "最大图数:", + "cpu": "cpu资源:{{val}}", + "memory": "内存资源:{{val}}", + "storage": "最大存储空间限制:", + "used": "已使用:", + "set_default": "默认图空间", + "created": "创建" + } + }, + "graph": { + "title": "图管理", + "create": "创建图", + "search_placeholder": "请输入图名称", + "unavailable": "图列表暂不可用,请检查 Server 连接后重试。", + "set_default_confirm": "确认更改图的默认设置?", + "set_default": { + "setting": "设置中...", + "success": "设置成功" + }, + "delete_confirm": { + "title": "确定删除图吗?", + "irreversible": "删除后无法恢复", + "deleting": "删除中", + "success": "删除成功", + "failed": "删除失败" + }, + "clear_confirm": { + "title": "确认清空图", + "graphspace": "GraphSpace:{{graphspace}}", + "graph": "图:{{graph}}", + "scope_data": "删除范围:图数据。清空前请先备份 Schema。", + "irreversible": "此操作不可恢复。", + "input_label": "输入图名以确认", + "input_placeholder": "图名", + "confirm": "清空图", + "cancel": "取消", + "request_failed": "清空失败" + }, + "col": { + "name": "图名称", + "auth": "是否开启鉴权", + "description": "描述", + "creator": "创建者", + "create_time": "创建时间", + "operation": "操作" + }, + "menu": { + "enter_analysis": "进入图分析平台", + "meta_config": "元数据配置", + "clear_data": "清空数据", + "set_default": "设为默认", + "view_schema": "查看schema", + "clone": "克隆图" + }, + "schema_view": { + "export": "导出 Groovy Schema", + "load_failed": "Schema 暂时无法加载,请检查 Server 连接后重试。", + "retry": "重试 Schema", + "export_failed": "Schema 导出失败,请在 Server 恢复后重试。" + }, + "form": { + "title_create": "创建图", + "title_edit": "编辑图", + "name": "图ID", + "name_placeholder": "只能包含小写字母、数字、_,最长48位", + "nickname": "图名", + "nickname_placeholder": "只能包含中文、字母、数字、_,最长48位", + "schema_placeholder": "请选择schema", + "schema_load_failed": "Schema 模板暂时无法加载。", + "schema_retry": "重试模板", + "schema_optional_hint": "可选;不选择模板时将创建空 Schema 图。", + "create_success": "创建成功", + "update_success": "更新成功", + "auth": "是否开启鉴权", + "description": "描述", + "description_placeholder": "图描述,可选" + }, + "clone": { + "unavailable": "当前连接的 Server 未提供兼容的克隆 API,因此暂不可用。", + "title": "克隆图", + "success": "克隆成功", + "name": "克隆图ID", + "name_duplicate": "不可以和原图ID重复", + "nickname_label": "克隆图名称", + "nickname": "{{nickname}}复制", + "nickname_duplicate": "不可以和原图名称重复", + "graphspace": "存储图空间", + "content": "克隆内容", + "schema": "克隆schema", + "schema_data": "克隆schema+数据", + "required_disk": "所需硬盘为", + "estimated_time": "预计复制时间" + }, + "detail": { + "title": "详情", + "name": "图名称", + "nickname": "图昵称", + "auth": "是否鉴权", + "graphspace": "所属图空间", + "creator": "创建者", + "create_time": "创建时间", + "last_update": "最近更新时间:", + "update_data": "数据更新", + "update_success": "已发起更新,请稍后刷新页面查看结果。", + "statistics_unavailable": "统计数据暂不可用,请稍后重试。", + "unavailable": "图详情暂不可用,请检查 Server 后重试。", + "vertex_total": "点总数", + "edge_total": "边总数", + "vertex_type": "点类型", + "edge_type": "边类型", + "count": "数量" + }, + "card": { + "more_actions": "{{graph}} 的更多操作", + "storage": "存储空间", + "detail_tooltip": "点击可以查看本图目前存储的点边的数量" + } + }, + "account": { + "title": "账号管理", + "load": { + "unavailable": "无法加载账号列表。", + "retry": "重试账号列表" + }, + "create": "创建账号", + "delete_confirm": "确定要删除账号 {{name}}吗?", + "col": { + "id": "账号ID", + "name": "账号名", + "remark": "备注", + "resource": "资源权限", + "create_time": "创建时间", + "is_superadmin": "是否为超级管理员", + "permission": "管理权限" + }, + "form": { + "title_detail": "查看账号", + "title_edit": "编辑账号", + "title_auth": "分配权限", + "title_create": "创建账号", + "id": "账号ID", + "id_placeholder": "用户登录", + "name": "账号名", + "name_placeholder": "账号名设置后可更改", + "is_superadmin": "是否为超级管理员", + "remark": "备注", + "remark_placeholder": "输入账号备注", + "default_password": "默认密码", + "default_password_placeholder": "123456(创建后可前往个人中心更改)", + "permission": "管理权限" + } + }, + "my": { + "title": "个人中心", + "edit_profile_title": "编辑信息", + "load": { + "unavailable": "无法加载个人资料。", + "retry": "重试个人资料" + }, + "col": { + "id": "账号ID", + "name": "账号名", + "is_superadmin": "是否为超级管理员", + "remark": "备注", + "permission": "管理权限", + "permission_roles": "权限及角色", + "create_time": "创建时间" + }, + "edit": { + "title": "更改密码", + "old_password": "旧密码", + "new_password": "新密码", + "confirm_password": "确认密码", + "old_password_placeholder": "请输入旧密码", + "new_password_placeholder": "请输入新密码", + "confirm_password_placeholder": "请再次输入密码", + "password_mismatch": "两次密码不一致", + "account_name_rule": "账号名不超过16个字符,且不能以下划线开始和结尾" + } + }, + "role": { + "title": "角色管理", + "create": "创建角色", + "delete_confirm": "确定删除角色{{name}}吗?", + "col": { + "name": "角色名称", + "description": "描述", + "operation": "操作" + }, + "form": { + "title_create": "创建角色", + "title_edit": "编辑角色", + "name": "角色名称", + "description": "描述", + "permissions": "权限" + }, + "auth": { + "title": "分配权限", + "graphspace": "图空间", + "graph": "图", + "permission": "权限" + } + }, + "resource": { + "title": "资源管理", + "create": "创建资源", + "col": { + "name": "资源名称", + "type": "类型", + "graphspace": "图空间", + "graph": "图", + "operation": "操作" + }, + "form": { + "title_create": "创建资源", + "title_edit": "编辑资源", + "name": "资源名称", + "type": "资源类型", + "graphspace": "图空间", + "graph": "图" + } + }, + "task": { + "title": "数据导入", + "search_placeholder": "请输入任务名称", + "pause": "暂停", + "execute": "执行", + "action": { + "detail": "查看执行历史", + "config": "查看任务配置", + "edit": "编辑任务", + "pause": "暂停任务", + "run": "运行任务", + "delete": "删除任务" + }, + "edit_title_create": "创建任务", + "edit_title_edit": "编辑任务", + "create": "创建任务", + "source": { + "file": "本地上传", + "hdfs": "HDFS", + "kafka": "Kafka", + "jdbc": "JDBC" + }, + "sync": { + "once": "执行一次", + "realtime": "实时执行", + "cron": "周期执行" + }, + "col": { + "name": "任务名称", + "source_type": "源数据类型", + "target_space": "目标图空间", + "target_graph": "目标图", + "sync_type": "同步类型", + "status": "状态", + "create_time": "创建时间", + "operation": "操作" + }, + "statistic": { + "total": "总任务", + "running": "运行中", + "success": "已完成", + "failed": "失败", + "realtime": "实时任务", + "offline": "非实时任务", + "pending": "待执行", + "executing": "正在执行", + "max_concurrency": "最大并发", + "once": "一次性任务", + "cron": "周期任务", + "realtime_task": "实时任务" + }, + "detail": { + "title": "任务详情", + "success": "完成", + "failed": "失败", + "running": "运行中", + "job_id": "执行实例 ID", + "import_count": "导入条数", + "create_time": "创建时间", + "average_rate": "平均速率", + "records_per_second": "{{rate}} 条/s", + "duration": "导入时长", + "seconds": "{{seconds}} 秒", + "status": "状态", + "other": "其它" + }, + "view": { + "title": "查看任务", + "unavailable": "无法加载当前任务详情。", + "retry": "重试任务详情", + "name": "任务名称", + "source_type": "源数据类型", + "target_space": "目标图空间", + "target_graph": "目标图", + "sync_type": "同步类型" + }, + "edit": { + "name": "任务名称", + "name_placeholder": "请输入任务名称", + "sync_type": "同步方式", + "cron_extra": "quartz表达式:秒 分钟 小时 天 月 星期 年(选填)", + "cron_required": "请输入调度信息", + "cron_placeholder": "定时 * 5 * * * * (*)", + "cron_rule": "请输入合法的quartz格式", + "update_success": "修改成功", + "graphspace": "图空间", + "graph": "图", + "source": "数据源", + "source_type": "源数据类型", + "basic_info": "基本信息", + "target": "目标", + "step_basic": "输入基础信息", + "step_source_fields": "选择源端字段", + "step_mapping_fields": "选择映射字段", + "step_schedule": "输入调度信息", + "graph_not_exist": "图不存在", + "datasource_failed": "无法加载所选数据源。", + "graphspace_failed": "无法加载所选图空间。", + "request_failed": "无法校验任务上下文。", + "retry_context": "重试校验", + "load_datasources_failed": "无法加载数据源。", + "retry_datasources": "重试数据源", + "load_graphspaces_failed": "无法加载图空间。", + "retry_graphspaces": "重试图空间", + "load_graphs_failed": "无法加载所选图空间中的图。", + "retry_graphs": "重试图列表", + "load_fields_failed": "无法加载所选数据源的字段。", + "retry_fields": "重试源端字段", + "submit_failed": "无法创建导入任务,请检查服务后重试。", + "graphspace_full": "{{graphspace}}图空间已达到最大存储容量,无法继续写入", + "duplicate_check_failed": "重名检查失败", + "duplicate_name": "任务名重复", + "name_rule": "只能包含中文、字母、数字、_, 不能超过20个字符", + "select_graphspace": "请选择图空间", + "select_graph": "请选择图", + "select_source": "请选择数据源", + "no_datasource": "暂无数据源", + "go_create_datasource": "立即去创建", + "select_source_type": "请选择源数据类型", + "select_source_fields": "请选择源端字段", + "source_available_fields": "源端可选字段", + "selected_fields": "已选字段", + "delete_field_confirm": "确定要删除这个字段吗?", + "add_field_placeholder": "添加字段,仅允许字母、数字、-、_", + "add_vertex_mapping": "新增顶点映射", + "add_edge_mapping": "新增边映射", + "type": "类型", + "vertex": "顶点", + "edge": "边", + "mapping_name": "名称", + "mapping_required_tip": "请至少创建一个顶点或边映射", + "create_vertex": "创建顶点", + "edit_vertex": "编辑顶点", + "create_edge": "创建边", + "edit_edge": "编辑边", + "vertex_type": "顶点类型", + "edge_type": "边类型", + "id_column": "ID列", + "source_id": "起点ID", + "target_id": "终点ID", + "property_mapping": "属性映射", + "value_mapping": "值映射", + "select_schema_field": "请选择schema字段", + "select_mapping_field": "请选择映射字段", + "duplicate_property": "属性不可重复", + "auto_match": "自动匹配", + "schedule_once": "执行一次", + "schedule_realtime": "实时执行", + "schedule_cron": "周期执行", + "load_full": "全量", + "load_incremental": "增量" + } + }, + "datasource": { + "title": "数据源管理", + "create": "新增数据源", + "delete": "删除数据源", + "search_placeholder": "输入数据源名称搜索", + "delete_title": "删除数据源", + "delete_content": "删除后,该数据源将从系统中消失。您确定要删除这个数据源吗?", + "selected_count": "已选中 {{selected}} 条 / 共 {{total}} 条", + "col": { + "name": "数据源名称", + "type": "数据源类型", + "host": "主机", + "port": "端口", + "username": "用户名", + "creator": "创建人", + "create_time": "创建时间", + "operation": "操作" + }, + "form": { + "upload": "上传文件", + "no_compress": "无", + "title_create": "新增数据源", + "title_edit": "编辑数据源", + "basic_info": "基础属性", + "config_info": "配置信息", + "auth_info": "认证信息", + "name": "数据源名称", + "name_placeholder": "请输入数据源名称", + "name_rule": "只能包含中文、字母、数字、_", + "type": "数据源类型", + "select_type": "请选择数据源类型", + "required": "请输入{{label}}", + "required_upload": "请上传{{label}}", + "upload_failed": "上传失败", + "auth_type": "特殊认证方式", + "auth_none": "无", + "auth_kerberos": "kerberos认证", + "keytab_file": "keytab文件", + "conf_file": "conf文件", + "local_upload": "本地上传", + "file_type": "文件类型", + "select": "请选择", + "header_placeholder": "请输入header,以,分割", + "delimiter": "列分隔符", + "charset": "编码字符集", + "date_format": "日期格式", + "time_zone": "时区", + "skipped_line": "跳过行", + "compression": "压缩格式", + "local_file": "本地文件", + "server_placeholder": "请输入kafka server list", + "topic_placeholder": "请输入topic", + "from_beginning": "是否从头读取", + "db_type": "数据库类型", + "select_db_type": "请选择数据库类型", + "url_placeholder": "请输入URL", + "url_rule": "请输入正确的jdbc url, 例如:jdbc:mysql://127.0.0.1:3306/db_name", + "database": "数据库名", + "database_placeholder": "请输入数据库名", + "schema_placeholder": "请输入schema", + "table": "表名", + "table_placeholder": "请输入表名", + "username": "用户名", + "username_placeholder": "请输入用户名", + "password": "密码", + "password_placeholder": "请输入密码", + "page_size": "页大小", + "where": "where语句", + "where_placeholder": "请输入where语句", + "test_connection": "连接测试", + "connection_success": "连接成功", + "connection_failed": "连接失败", + "connection_unsupported": "本地暂不支持 JDBC 数据源连接测试" + } + }, + "schema": { + "list_failed": "Schema 数据暂时无法加载,请检查 Server 连接后重试。", + "retry": "重试 Schema 数据", + "delete_failed": "删除失败,请检查 Server 连接后重试。", + "title": "元数据管理", + "identity": { + "graph_unavailable": "无法加载当前图信息。", + "graphspace_unavailable": "无法加载当前 GraphSpace 信息。", + "retry": "重试图上下文" + }, + "name_placeholder": "允许出现中英文、数字、下划线", + "delete_irreversible": "删除后无法恢复,请谨慎操作。", + "delete_task_hint": "删除元数据耗时较久,详情可在任务管理中查看", + "col": { + "label_index": "类型索引", + "properties": "关联属性", + "property_indexes": "属性索引" + }, + "validation": { + "duplicate_index": "索引名称不可重复", + "duplicate_property": "属性不可重复" + }, + "common": { + "add_success": "添加成功", + "batch_delete": "批量删除", + "delete_warning": "删除后无法恢复,请谨慎操作。", + "name_placeholder": "允许出现中英文、数字、下划线" + }, + "options": { + "size_tiny": "超小", "size_small": "小", "size_normal": "中", "size_big": "大", "size_huge": "超大", + "line_thick": "粗", "line_fine": "细", + "id_primary": "主键ID", "id_auto": "自动生成", "id_string": "自定义字符串", "id_number": "自定义数字", "id_uuid": "自定义UUID", + "required": "非空", "nullable": "允许空", + "index_secondary": "二级索引", "index_range": "范围索引", "index_search": "全文索引", "index_unique": "唯一索引" + }, + "tab": { + "property": "属性", + "vertex": "顶点类型", + "edge": "边类型", + "vertex_index": "顶点索引", + "edge_index": "边索引" + }, + "property": { + "create": "创建属性", + "delete_confirm": "确认删除此属性?", + "in_use": "属性数据 {{names}} 正在使用中,不可删除", + "col": { + "name": "属性名称", + "type": "数据类型", + "cardinality": "基数", + "operation": "操作" + }, + "form": { + "name": "属性名称:", + "type": "数据类型:", + "cardinality": "基数:" + } + }, + "vertex": { + "create": "创建顶点类型", + "edit": "编辑顶点类型", + "detail_failed": "顶点类型详情暂时无法加载,请重试后再编辑。", + "id": "顶点ID", + "in_use": "顶点数据 {{names}} 正在使用中,不可删除", + "select_properties_first": "请先选择关联属性", + "style": "顶点样式:", + "display_fields": "顶点展示内容", + "delete_confirm": "确认删除此顶点类型?", + "delete_warning": "删除后无法恢复,请谨慎操作", + "delete_task_note": "删除元数据耗时较久,详情可在任务管理中查看", + "col": { + "name": "顶点类型名称", + "id_strategy": "ID策略", + "properties": "关联属性", + "primary_keys": "主键属性", + "label_index": "类型索引", + "property_index": "属性索引", + "operation": "操作" + }, + "form": { + "title_create": "创建顶点类型", + "title_edit": "编辑顶点类型", + "name": "顶点类型名称:", + "id_strategy": "ID策略:", + "style": "顶点样式:", + "display_fields": "顶点展示内容", + "label_index": "类型索引", + "property_index": "属性索引", + "properties": "关联属性" + } + }, + "edge": { + "create": "创建边类型", + "edit": "编辑边类型", + "detail_failed": "边类型详情暂时无法加载,请重试后再编辑。", + "display_fields": "边展示内容", + "link_multi_times": "允许多次连接", + "parent": "父边类型:", + "select_parent": "请选择父边", + "style": "边样式:", + "delete_confirm": "确认删除此边类型?", + "delete_warning": "删除后无法恢复,请谨慎操作", + "delete_task_note": "删除元数据耗时较久,详情可在任务管理中查看", + "col": { + "name": "边类型名称", + "type": "边类型", + "source": "起点类型", + "target": "终点类型", + "properties": "关联属性", + "sort_keys": "区分键", + "label_index": "类型索引", + "property_index": "属性索引", + "operation": "操作" + }, + "type": { + "normal": "普通类型", + "parent": "父边类型", + "sub": "子边类型", + "NORMAL": "普通类型", + "PARENT": "父类型", + "SUB": "子类型", + "parent_label": "父边:" + }, + "form": { + "title_create": "创建边", + "title_edit": "编辑边", + "name": "边类型名称:", + "type": "类型:", + "type_normal": "普通类型", + "type_parent": "父边类型", + "type_sub": "子边类型", + "parent": "父边类型:", + "parent_placeholder": "请选择父边", + "style": "顶点样式:", + "source": "起点类型", + "target": "终点类型", + "multi_link": "允许多次连接", + "properties": "关联属性", + "sort_keys": "区分键", + "display_fields": "边展示内容", + "label_index": "类型索引", + "property_index": "属性索引" + } + }, + "index": { + "vertex_title": "顶点索引", + "edge_title": "边索引", + "create": "创建索引", + "col": { + "name": "索引名称", + "type": "索引类型", + "fields": "属性", + "operation": "操作" + } + }, + "image_view": { + "load_failed": "Schema 图数据暂时无法加载,请重试后再编辑。", + "vertex": "顶点类型", + "edge": "边类型", + "view_properties": "查看属性" + } + }, + "schema_template": { + "title": "{{name}} - Schema 模板管理", + "create": "创建 Schema 模板", + "search_placeholder": "请输入 Schema 模板名称", + "delete_confirm": "确定要删除 {{name}} 吗?", + "delete_success": "删除成功", + "delete_failed": "删除失败", + "create_duplicate": "Schema 模板 {{name}} 已存在,请使用其他名称。", + "create_failed": "无法创建 Schema 模板,请检查输入后重试。", + "update_failed": "无法更新 Schema 模板,请检查输入后重试。", + "create_success": "新增成功", + "update_success": "编辑成功", + "action": { + "view": "查看", + "edit": "编辑", + "create": "创建", + "delete": "删除" + }, + "column": { + "name": "Schema 模板名称", + "created_at": "创建时间", + "updated_at": "更新时间", + "creator": "创建人", + "operation": "操作" + }, + "form": { + "name": "Schema 模板名称", + "name_placeholder": "请输入 Schema 名称", + "schema_placeholder": "请输入 Schema" + } + }, + "navigation_page": { + "title": "导航", + "step1": "创建数据", + "step2": "业务分析", + "step3": "系统管理", + "step4": "业务管理", + "create_graph": "创建图", + "create_schema": "创建Schema", + "load_data": "数据入库", + "graph_query": "图语言分析", + "graph_algorithm": "图算法分析", + "graph_manage": "图空间管理", + "data_manage": "数据管理", + "account_manage": "账号管理", + "role_manage": "角色管理", + "resource_manage": "资源管理", + "task_manage": "任务管理", + "datasource_manage": "数据源管理", + "operation_manage": "运维管理", + "cluster_manage": "集群管理", + "monitor_manage": "监控管理", + "node_manage": "节点运维", + "alert_manage": "报警管理", + "dashboard_checking": "正在检查 Dashboard 可用性……", + "dashboard_unavailable": "Dashboard 不可用,请检查 dashboard.address 配置和服务健康状态。", + "dashboard_popup_blocked": "Dashboard 窗口被拦截,请允许弹出式窗口后重试。" + }, + "not_found": { + "subtitle": "页面不存在", + "home": "返回首页" + }, + "login": { + "submit": "登录", + "username": "用户名", + "password": "密码", + "username_required": "请输入用户名。", + "password_required": "请输入密码。", + "title": "登录 HugeGraph" + } +} diff --git a/hugegraph-hubble/hubble-fe/src/index-router.test.js b/hugegraph-hubble/hubble-fe/src/index-router.test.js new file mode 100644 index 000000000..48b54f041 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/index-router.test.js @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import fs from 'fs'; +import path from 'path'; + +test('production router opts into validated v7 transition behavior', () => { + const source = fs.readFileSync(path.join(__dirname, 'index.js'), 'utf8'); + + expect(source).toContain('v7_startTransition: true'); + expect(source).toContain('v7_relativeSplatPath: true'); +}); diff --git a/hugegraph-hubble/hubble-fe/src/index.css b/hugegraph-hubble/hubble-fe/src/index.css new file mode 100644 index 000000000..54af9a485 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/index.css @@ -0,0 +1,36 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +body { + margin: 0; + font-family: + PingFangSC-Regular, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/hugegraph-hubble/hubble-fe/src/index.js b/hugegraph-hubble/hubble-fe/src/index.js new file mode 100644 index 000000000..37d0ea877 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/index.js @@ -0,0 +1,51 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import {BrowserRouter} from 'react-router-dom'; +import {ConfigProvider} from 'antd'; +// FIXME: Consolidate moment into date-fns after replacing filter comparisons +// and Ant Design date values together; removing it piecemeal changes semantics. +import 'moment/locale/zh-cn'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; +import zhCN from 'antd/lib/locale/zh_CN'; +import enUS from 'antd/lib/locale/en_US'; +import './i18n'; + +const languageType = localStorage.getItem('languageType') === 'en-US' ? enUS : zhCN; +const routerFuture = { + v7_startTransition: true, + v7_relativeSplatPath: true, +}; +const root = ReactDOM.createRoot(document.getElementById('root')); + +root.render( + + + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/hugegraph-hubble/hubble-fe/src/index.less b/hugegraph-hubble/hubble-fe/src/index.less deleted file mode 100644 index b0e16a767..000000000 --- a/hugegraph-hubble/hubble-fe/src/index.less +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -@import '~hubble-ui/src/index.less'; - -@primary-color: #2b65ff; -@primary-hover-color: #527dff; -@primary-active-color: #184bcc; - -* { - box-sizing: border-box; -} - -html, -body, -img, -ul, -ol, -li, -table, -tr, -th, -td, -p { - margin: 0; - padding: 0; - border: 0; -} - -body { - background: #f2f2f2; - // prettier-ignore - font-family: - -apple-system, - BlinkMacSystemFont, - 'Helvetica Neue', - Helvetica, - 'PingFang SC', - 'Hiragino Sans GB', - 'Microsoft YaHei', - '微软雅黑', - Arial, - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - &.dark { - background: #000; - } - - &::after { - position: absolute; - overflow: hidden; - left: -20000px; - // preload images which in hover state to solve blink issue - content: - url(./assets/imgs/ic_topback.svg) - url(./assets/imgs/ic_arrow_white.svg) - url(./assets/imgs/ic_shuju_normal.svg) - url(./assets/imgs/ic_shuju_pressed.svg) - url(./assets/imgs/ic_yuanshuju_normal.svg) - url(./assets/imgs/ic_yuanshuju_pressed.svg) - url(./assets/imgs/ic_cebianzhankai.svg) - url(./assets/imgs/ic_cebianshouqi.svg) - url(./assets/imgs/ic_daorushuju_normal.svg) - url(./assets/imgs/ic_daorushuju_pressed.svg) - url(./assets/imgs/ic_renwuguanli_normal.svg) - url(./assets/imgs/ic_renwuguanli_pressed.svg) - url(./assets/imgs/ic_tuzhanshi_normal.svg) - url(./assets/imgs/ic_tuzhanshi_hover.svg) - url(./assets/imgs/ic_tuzhanshi_pressed.svg) - url(./assets/imgs/ic_biaoge_normal.svg) - url(./assets/imgs/ic_biaoge_hover.svg) - url(./assets/imgs/ic_biaoge_pressed.svg) - url(./assets/imgs/ic_json_normal.svg) url(./assets/imgs/ic_json_hover.svg) - url(./assets/imgs/ic_json_pressed.svg) url(./assets/imgs/ic_close_16.svg) - url(./assets/imgs/ic_refresh.svg) url(./assets/imgs/ic_loading_back.svg) - url(./assets/imgs/ic_loading_front.svg) - url(./assets/imgs/ic_question_mark.svg); - } -} - -code { - // prettier-ignore - font-family: - source-code-pro, - Menlo, - Monaco, - Consolas, - 'Courier New', - monospace; -} - -.tooltips { - background: #fff; - box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2); - border-radius: 4px; - padding: 28px 24px 24px; -} - -.tooltips-dark { - border-radius: 3px; - background-color: rgba(0, 0, 0, 0.8); - border-color: transparent; - padding: 4px 8px; - color: #fff; - line-height: 18px; - font-size: 12px; - max-width: 240px; - word-wrap: break-word; - - // override tooltip-arrow - .tooltip-arrow[data-placement*='bottom'] { - margin-top: -0.25rem; - } - - .tooltip-arrow[data-placement*='bottom']::before { - border-color: transparent transparent rgba(0, 0, 0, 0.8) transparent; - border-width: 0 0.3rem 0.3rem 0.3rem; - position: absolute; - top: 0; - } - - .tooltip-arrow[data-placement*='bottom']::after { - border-color: transparent transparent rgba(0, 0, 0, 0.8) transparent; - // border-color: rgba(0, 0, 0, 0.8); - border-width: 0 0.3rem 0.3rem 0.3rem; - } -} - -.no-line-break { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.message-wrapper { - font-size: 14px; - line-height: 22px; - - &-title { - font-weight: 900; - margin: 3.6px 0 4px; - } - - &-manipulation { - cursor: pointer; - color: #2b65ff; - } -} - -/* overrides (global) */ - -//primary button background colors -.new-fc-one-btn-primary { - background: @primary-color; - - &:hover { - background: @primary-hover-color; - } - - &:active { - background: @primary-active-color; - } - - &-disabled { - background: #d9e6ff; - - &:hover, - &:active { - background: #d9e6ff; - } - } -} - -// Modal titles -.new-fc-one-modal-title { - // prettier-ignore - font-family: - 'PingFangSC-Medium', - 'Microsoft YaHei', - '微软雅黑', - Arial, - sans-serif; - font-weight: bold; - font-size: 16px; -} - -// drawers, e.g. metadata-configs vertex/edge types -.new-fc-one-drawer-wrapper-body-small .new-fc-one-drawer-title { - font-size: 16px; -} - -.new-fc-one-drawer-close { - top: 28px; -} - -// z-index input errorlayer in Drawer should be higher -// prettier-ignore -.new-fc-one-drawer-content-wrapper .new-fc-one-input-error.new-fc-one-input-error-layer { - z-index: 1041; -} - -// also z-index is invalid if parent node has position: relative -.new-fc-one-drawer-content-wrapper .new-fc-one-input-all-container { - position: absolute; -} - -// Steps process style -.new-fc-one-steps-item-process .new-fc-one-steps-item-icon { - background: @primary-color; -} - -// checked-color -.new-fc-one-switch-checked { - background: @primary-color; - - &:hover { - background: @primary-hover-color; - } -} - -// text in -.new-fc-one-switch-inner { - margin-right: 0; -} - -.new-fc-one-switch-checked .new-fc-one-switch-inner { - margin-left: 0; -} - -// override radio button primary color -.new-fc-one-radio-button-wrapper-checked { - border: 1px solid @primary-color; - background-color: @primary-color; - color: #fff; - - &:hover { - border: 1px solid @primary-hover-color; - background-color: @primary-hover-color; - color: #fff; - } -} - -// hovered color -.new-fc-one-menu-horizontal-box .new-fc-one-menu-item-selected:hover { - color: @primary-hover-color; - border-color: @primary-hover-color; -} - -//
    th checkbox color -.new-fc-one-checkbox-wrapper .new-fc-one-checkbox-indeterminate .new-fc-one-checkbox-inner { - background-color: #2b65ff; -} - -.new-fc-one-table-thead > tr > th:first-of-type { - padding: 12px 16px 12px 20px; -} - -// could be a bug where container style is display: none -.new-fc-one-message { - display: block !important; -} - -// proper line break -.new-fc-one-message-container-content { - word-break: break-word; -} - -// -.new-fc-one-breadcrumb-link { - color: #2b65ff; -} - -// -.new-fc-one-modal-body { - overflow-y: auto !important; -} - -// menu center -.data-analyze-sidebar .ant-menu.ant-menu-inline-collapsed > .ant-menu-item { - padding: 0 12px; -} diff --git a/hugegraph-hubble/hubble-fe/src/index.tsx b/hugegraph-hubble/hubble-fe/src/index.tsx deleted file mode 100644 index 2c6b3b7c8..000000000 --- a/hugegraph-hubble/hubble-fe/src/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.less'; -import App from './components/App'; -import './i18n'; -import { ConfigProvider } from 'antd'; -import zhCN from 'antd/lib/locale/zh_CN'; -import enUS from 'antd/lib/locale/en_US'; - -// UI component has built-in text internationalization, -// such as confirmation, cancellation, etc -const languageType = - localStorage.getItem('languageType') === 'en-US' ? enUS : zhCN; -ReactDOM.render( - - - , - document.getElementById('root') -); diff --git a/hugegraph-hubble/hubble-fe/src/layout.ant.js b/hugegraph-hubble/hubble-fe/src/layout.ant.js new file mode 100644 index 000000000..f953b9c53 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/layout.ant.js @@ -0,0 +1,41 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {Layout} from 'antd'; +import Sidebar from './components/Sidebar/index.ant'; +import Topbar from './components/Topbar/index.ant'; +import {Outlet} from 'react-router-dom'; +import 'antd/dist/antd.css'; + +const LayoutAnt = () => { + return ( + + + + + + + + + + + + ); +}; + +export default LayoutAnt; diff --git a/hugegraph-hubble/hubble-fe/src/logo.svg b/hugegraph-hubble/hubble-fe/src/logo.svg new file mode 100644 index 000000000..eb0b8d315 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/logo.svg @@ -0,0 +1,19 @@ + + + diff --git a/hugegraph-hubble/hubble-fe/src/modules/Context/index.js b/hugegraph-hubble/hubble-fe/src/modules/Context/index.js new file mode 100644 index 000000000..d0a5f3145 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/Context/index.js @@ -0,0 +1,36 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图分析部分Context + */ + + +import React from 'react'; + +const defaultContext = { + graphSpace: null, + graph: null, + graphLoadTime: null, + graphStatus: null, + isVermeer: false, +}; + +const GraphAnalysisContext = React.createContext(defaultContext); + +export default GraphAnalysisContext; diff --git a/hugegraph-hubble/hubble-fe/src/modules/GraphAnalysis/index.js b/hugegraph-hubble/hubble-fe/src/modules/GraphAnalysis/index.js new file mode 100644 index 000000000..5ea5910a9 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/GraphAnalysis/index.js @@ -0,0 +1,152 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import React, {useState, useCallback, useEffect} from 'react'; +import {PageHeader, message} from 'antd'; +import AnalysisHome from '../analysis/Home'; +import AlgorithmHome from '../algorithm/Home'; +import AsyncTaskHome from '../asyncTasks/Home'; +import GraphAnalysisContext from '../Context'; +import TopBar from '../component/TopBar'; +import {GRAPH_ANALYSIS_MODULE} from '../../utils/constants'; +import _ from 'lodash'; +import * as api from '../../api'; +import {useTranslation} from 'react-i18next'; + +const {GREMLIN, ALGORITHMS, ASYNCTASKS} = GRAPH_ANALYSIS_MODULE; + +const pageHeaderNameKeys = { + [GREMLIN]: 'analysis.query.name', + [ALGORITHMS]: 'analysis.algorithm.name', + [ASYNCTASKS]: 'analysis.async_task.name', +}; + +const GraphAnalysisHome = props => { + const {moduleName} = props; + const {t} = useTranslation(); + + const [currentOlapMode, setCurrentOlapMode] = useState(false); + const [isOlapModeLoading, setOlapModeLoading] = useState(false); + const [context, setContext] = useState( + { + graphSpace: null, + graph: null, + graphLoadTime: null, + graphStatus: null, + isVermeer: false, + } + ); + + const renderModule = () => { + switch (moduleName) { + case GREMLIN: + return ; + case ALGORITHMS: + return ; + case ASYNCTASKS: + return ; + default: + break; + } + }; + + const onOlapModeChange = useCallback( + async open => { + const {graphSpace, graph} = context; + setOlapModeLoading(true); + const response = await api.analysis.switchOlapMode(graphSpace, graph, open ? 0 : 1); + if (response.status === 200) { + setCurrentOlapMode(open); + } + setOlapModeLoading(false); + }, + [context] + ); + + const getCurrentOlapMode = useCallback( + async (graphSpace, graph) => { + setOlapModeLoading(true); + const response = await api.analysis.getOlapMode(graphSpace, graph); + const {status, data} = response; + if (status === 200) { + const {status: currentOlapStatus} = data; + setCurrentOlapMode(currentOlapStatus === '0'); + } + setOlapModeLoading(false); + }, + [] + ); + + const onGraphInfoChange = useCallback( + (graphSpace, graph) => { + const {name: currentGraph, last_load_time, status} = graph; + if (graphSpace && !_.isEmpty(graph)) { + getCurrentOlapMode(graphSpace, currentGraph); + } + setContext(context => ({ + ...context, + graphSpace, + graph: currentGraph, + graphLoadTime: last_load_time, + graphStatus: status, + })); + }, + [getCurrentOlapMode] + ); + + useEffect( + () => { + (async () => { + const response = await api.auth.getVermeer(); + const {status, data, message: errMsg} = response || {}; + const {enable} = data || {}; + if (status === 200) { + setContext(context => ( + { + ...context, + isVermeer: enable || false, + } + )); + } + else { + !errMsg && message.error(t('analysis.topbar.get_vermeer_failed')); + } + })(); + }, + [t] + ); + + return ( + + + + + {renderModule()} + + ); +}; + +export default GraphAnalysisHome; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/AlgorithmSearch/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/AlgorithmSearch/index.js new file mode 100644 index 000000000..28ac8acfd --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/AlgorithmSearch/index.js @@ -0,0 +1,54 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图算法 搜索 + */ + +import React, {useMemo} from 'react'; +import {Input} from 'antd'; +import _ from 'lodash'; +import c from './index.module.scss'; +import {useTranslation} from 'react-i18next'; + +const AlgorithmSearch = props => { + const {t} = useTranslation(); + const {onSearch} = props; + + const debounceOnChange = useMemo( + () => { + return _.debounce(e => { + const {value} = e.target; + onSearch(value); + }, 200); + }, + [onSearch] + ); + + return ( +
    + +
    + ); +}; + +export default AlgorithmSearch; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/AlgorithmSearch/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/AlgorithmSearch/index.module.scss new file mode 100644 index 000000000..405f98417 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/AlgorithmSearch/index.module.scss @@ -0,0 +1,27 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.algorithmSearch{ + :global(.ant-input-affix-wrapper){ + width: 250px; + height: 40px; + border: 1px solid #E7E8E9; + margin: -1px 0 0 -1px; + padding: 4px 10px; + } +} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/GraphMenuBar/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/GraphMenuBar/index.js new file mode 100644 index 000000000..c41cd40a1 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/GraphMenuBar/index.js @@ -0,0 +1,200 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file GraphMenuBar(图算法) + */ + +import React, {useCallback} from 'react'; +import {useTranslation} from 'react-i18next'; +import MenuBar from '../../../component/MenuBar'; +import ImportData from '../../../component/ImportData'; +import ExportData from '../../../component/ExportData'; +import StyleConfig from '../../../component/styleConfig/Home'; +import FilterHome from '../../../component/filter/Home'; +import LayoutConfig from '../../../component/LayoutConfig'; +import SettingConfig from '../../../component/SettingConfig'; +import NewConfig from '../../../component/NewConfig'; +import Statistics from '../../../component/Statistics'; +import RenderModeSwitcher from '../../../component/GraphRenderModeSwitcher'; +import {PANEL_TYPE, GRAPH_RENDER_MODE} from '../../../../utils/constants'; +import {formatToDownloadData} from '../../../../utils/formatGraphResultData'; +import useDownloadJson from '../../../../customHook/useDownloadJson'; + +const {LAYOUT, SETTING, STATISTICS} = PANEL_TYPE; +const {CANVAS2D} = GRAPH_RENDER_MODE; + +const GraphMenuBar = props => { + const {t} = useTranslation(); + const { + styleConfigData, + graphData, + handleImportData, + handleExportPng, + handleGraphStyleChange, + handleFilterChange, + handleTogglePanel, + handleClickNewAddNode, + handleClickNewAddEdge, + handleSwitchRenderMode, + refreshExcuteCount, + showCanvasInfo, + graphRenderMode, + } = props; + + const {downloadJsonHandler} = useDownloadJson(); + + const isCanvas2D = graphRenderMode === CANVAS2D; + const buttonEnableForCanvas2D = showCanvasInfo && isCanvas2D; + const buttonEnableForImport = !showCanvasInfo && isCanvas2D; + + const handleExportJson = useCallback( + fileName => { + downloadJsonHandler(fileName, formatToDownloadData(graphData)); + }, + [downloadJsonHandler, graphData] + ); + const handleToggleLayout = useCallback( + () => handleTogglePanel(LAYOUT), + [handleTogglePanel] + ); + const handleToggleSetting = useCallback( + () => handleTogglePanel(SETTING), + [handleTogglePanel] + ); + const handleToggleStatistics = useCallback( + () => handleTogglePanel(STATISTICS), + [handleTogglePanel] + ); + + const menubarContent = [ + { + key: 1, + content: ( + + ), + }, + { + key: 2, + content: ( + ), + }, + { + key: 3, + content: ( + ), + }, + { + key: 4, + content: ( + + ), + }, + { + key: 5, + content: ( + ), + }, + { + key: 6, + content: ( + ), + }, + { + key: '7', + content: ( + ), + }, + { + key: 8, + content: ( + ), + }, + ]; + + const menubarExtras = [{ + key: 1, + content: ( + + ), + }]; + + return ( + + ); +}; + +export default GraphMenuBar; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/GraphToolBar/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/GraphToolBar/index.js new file mode 100644 index 000000000..74d106b51 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/GraphToolBar/index.js @@ -0,0 +1,71 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file GraphToolBar + */ + +import React, {useState, useCallback} from 'react'; +import ToolBar from '../../../component/ToolBar'; +import ZeroDegreeNodeSearch from '../../../component/ZeroDegreeNode'; +import RedoUndo from '../../../component/RedoUndo'; +import FitCenter from '../../../component/FitCenter'; +import ZoomGraph from '../../../component/ZoomGraph'; +import ClearGraph from '../../../component/ClearGraph'; +import FullScreen from '../../../component/FullScreen'; +import RefreshGraph from '../../../component/RefreshGraph'; +import FixNode from '../../../component/FixNode'; +import {PANEL_TYPE} from '../../../../utils/constants'; + +const {CLOSED} = PANEL_TYPE; + +const GraphToolBar = props => { + const { + handleRedoUndoChange, + handleClearGraph, + panelType, + updatePanelType, + } = props; + + const [isFullScreen, setFullScreen] = useState(false); + + const handleChangeFullScreen = useCallback( + () => { + setFullScreen(pre => !pre); + updatePanelType(CLOSED); + }, + [updatePanelType] + ); + + const toolBarExtras = [ + {key: '1', content: ()}, + {key: '2', content: ()}, + {key: '3', content: ()}, + {key: '45', content: ()}, + {key: '6', content: ()}, + {key: '7', content: ()}, + {key: '89', content: ()}, + {key: '10', content: ()}, + ]; + + return ( + + ); +}; + +export default GraphToolBar; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js new file mode 100644 index 000000000..52bd40448 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js @@ -0,0 +1,558 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图分析画布 Home + */ +import React, {useCallback, useEffect, useState, useContext, useMemo, useRef} from 'react'; +import {useTranslation} from 'react-i18next'; +import GraphAnalysisContext from '../../../Context'; +import Graph from '../../../component/Graph'; +import Legend from '../../../component/Legend'; +import MiniMap from '../../../component/MiniMap'; +import GraphMenuBar from '../GraphMenuBar'; +import Tooltip from '../../../component/Tooltip'; +import GraphToolBar from '../GraphToolBar'; +import Menu from '../../../component/Menu'; +import NumberCard from '../../../component/NumberCard'; +import EditElement from '../../../component/EditElement'; +import Search from '../../../component/Search'; +import SettingConfigPanel from '../../../component/SettingConfigPanel'; +import LayoutConfigPanel from '../../../component/layoutConfigPanel/Home'; +import PanelControlButton from '../../../component/ClosePanelButton'; +import DynamicAddNode from '../../../component/DynamicAddNode'; +import DynamicAddEdge from '../../../component/DynamicAddEdge'; +import NeighborRankApiView from '../NeighborRankView'; +import StatisticPanel from '../../../component/StatisticsPanel/Home'; +import RankApiView from '../RankApiView'; +import JaccView from '../JaccView'; +import GraphStatusView from '../../../component/GraphStatusView'; +import TaskNavigateView from '../../../component/TaskNavigateView'; +import Canvas3D from '../../../component/Canvas3D'; +import {filterData} from '../../../../utils/filter'; +import {formatToGraphData, formatToOptionedGraphData, formatToStyleData, + formatToDownloadData, updateGraphDataStyle, formatToLegendData} from '../../../../utils/formatGraphResultData'; +import {fetchExpandInfo, handleAddGraphNode, handleAddGraphEdge, handleExpandGraph} from '../utils'; +import {mapLayoutNameToLayoutDetails} from '../../../../utils/graph'; +import { + GRAPH_STATUS, PANEL_TYPE, GRAPH_RENDER_MODE, ALGORITHM_NAME, Algorithm_Layout, + getAlgorithmDisplayName, getCanonicalAlgorithmName, +} from '../../../../utils/constants'; +import c from './index.module.scss'; +import _ from 'lodash'; + +const GraphResult = props => { + const {t} = useTranslation(); + const { + data = {vertexs: [], edges: []}, + metaData, + options, + asyncTaskId, + queryStatus, + queryMessage, + isQueryMode, + algorithm: algorithmName, + resetGraphStatus, + panelType, + updatePanelType, + graphNums, + propertyKeysRecords, + graphRenderMode, + onGraphRenderModeChange, + } = props; + const canonicalAlgorithmName = useMemo( + () => getCanonicalAlgorithmName(algorithmName, t), + [algorithmName, t] + ); + const displayAlgorithmName = getAlgorithmDisplayName(algorithmName, t); + + const {STANDBY, LOADING, SUCCESS, FAILED, UPLOAD_FAILED} = GRAPH_STATUS; + const {JACCARD_SIMILARITY, JACCARD_SIMILARITY_POST, RANK_API, + NEIGHBOR_RANK_API, ADAMIC_ADAR, RESOURCE_ALLOCATION} = ALGORITHM_NAME; + const noneGraphAlgorithm = [JACCARD_SIMILARITY, JACCARD_SIMILARITY_POST, RANK_API, + NEIGHBOR_RANK_API, ADAMIC_ADAR, RESOURCE_ALLOCATION]; + const {CLOSED, LAYOUT, SETTING, STATISTICS} = PANEL_TYPE; + const {CANVAS2D} = GRAPH_RENDER_MODE; + + + const graphSpaceInfo = useContext(GraphAnalysisContext); + const {edgeMeta, vertexMeta} = metaData || {}; + const [graphData, setGraphData] = useState({nodes: [], edges: []}); + const [styleConfigData, setStyleConfigData] = useState({nodes: {}, edges: {}}); + const [showAddNodeDrawer, setShowAddNodeDrawer] = useState(false); + const [showAddEdgeDrawer, setShowAddEdgeDrawer] = useState(false); + const [isClickNew, setIsClickNew] = useState(false); + const [searchVisible, setSearchVisible] = useState(false); + const [searchVertex, setSearchVertex] = useState({}); + const [addEdgeDrawerInfo, setAddEdgeDrawerInfo] = useState({}); + const [isOutEdge, setOutEdge] = useState(false); + const [showEditElement, setShowEditElement] = useState(false); + const [editElementInfo, setEditElementInfo] = useState(); + const [graph, setGraph] = useState(); + const [graphAllInfo, setGraphAllInfo] = useState(); + const excuteStyleChangeCount = useRef(0); + const {jaccardsimilarity, rankObj, rankArray} = options || {}; + const showCanvasInfo = (_.size(data.vertices) !== 0 || _.size(data.edges) !== 0) && queryStatus === SUCCESS; + const layoutInfo = useMemo( + () => mapLayoutNameToLayoutDetails({ + layout: Algorithm_Layout[canonicalAlgorithmName], + startId: options?.startId, + }), + [canonicalAlgorithmName, options?.startId] + ); + + const onGraphRender = useCallback(graph => { + setGraph(graph); + }, []); + + useEffect(() => { + setGraphAllInfo(graphNums); + }, [graphNums]); + + useEffect( + () => { + const rawGraphData = formatToGraphData(data, metaData, {}); + const finalGraphData = formatToOptionedGraphData(rawGraphData, options, canonicalAlgorithmName); + const styleConfigData = formatToStyleData(rawGraphData); + setGraphData(finalGraphData); + setStyleConfigData(styleConfigData); + }, + [canonicalAlgorithmName, data, metaData, options] + ); + + const handleUpdateStatus = useCallback( + (status, message, result) => { + resetGraphStatus && resetGraphStatus(status, message, result); + }, + [resetGraphStatus] + ); + + const handleExportPng = useCallback( + fileName => { + graph.downloadFullImage(fileName, 'image/png', {backgroundColor: '#FFF', padding: 30}); + }, + [graph] + ); + + const handleGraphStyleChange = useCallback( + styleConfigData => { + try { + const styledData = updateGraphDataStyle(graphData, styleConfigData); + const newGraphData = formatToOptionedGraphData(styledData, options, canonicalAlgorithmName); + graph.changeData(_.cloneDeep(newGraphData), true); + graph.getNodes().forEach(item => { + graph.refreshItem(item); + if (item.hasLocked()) { + graph.setItemState(item, 'customFixed', true); + } + }); + setGraphData({...styledData}); + setStyleConfigData(styleConfigData); + } + catch (err) { + if (excuteStyleChangeCount.current > 2) { + throw new Error(err); + } + else { + excuteStyleChangeCount.current++; + handleGraphStyleChange(styleConfigData); + } + } + }, + [canonicalAlgorithmName, graph, graphData, options] + ); + + const handleRefreshExcuteCount = useCallback(() => { + excuteStyleChangeCount.current = 0; + }, []); + + const handleFilterChange = useCallback( + values => { + const {filter} = values; + const newData = filterData(props.data, filter.rules, filter.logic); + const newRawGraphData = formatToGraphData(newData || {}, metaData, styleConfigData); + const newGraphData = formatToOptionedGraphData(newRawGraphData, options, canonicalAlgorithmName); + graph.changeData(newGraphData, true); + graph.refresh(); + setGraphData(newGraphData); + setStyleConfigData(formatToStyleData(newRawGraphData)); + }, + [canonicalAlgorithmName, graph, metaData, options, props.data, styleConfigData] + ); + + const handleTogglePanel = useCallback( + type => { + if (panelType === type) { + updatePanelType(CLOSED); + } + else { + updatePanelType(type); + } + }, [CLOSED, panelType, updatePanelType] + ); + + const handleLayoutChange = useCallback( + layout => { + graph.destroyLayout(); + graph.updateLayout(layout, 'center', undefined, false); + }, + [graph] + ); + + const handleSettingChange = useCallback( + changedData => { + graph.changeData(_.cloneDeep(changedData), false); + graph.refresh(); + setGraphData({...changedData}); + }, [graph] + ); + + const handleClickNewAddNode = useCallback( + () => { + setShowAddNodeDrawer(true); + }, []); + + const handleClickNewAddEdge = useCallback( + isOut => { + setIsClickNew(true); + setShowAddEdgeDrawer(true); + setOutEdge(isOut); + }, []); + + const handleClickGraphNode = useCallback( + value => { + setShowEditElement(true); + setEditElementInfo(value.getModel()); + }, []); + + const handleExpand = useCallback( + (newData, graphInstance) => { + const newGraphData = handleExpandGraph(newData, metaData, + styleConfigData, options, canonicalAlgorithmName, graphInstance); + setGraphData(newGraphData); + setStyleConfigData(formatToStyleData(newGraphData)); + }, [canonicalAlgorithmName, metaData, options, styleConfigData]); + + const getExpandInfo = useCallback( + async (params, graphInstance) => { + const searchResultRaw = await fetchExpandInfo(params, graphInstance, graphSpaceInfo); + handleExpand(searchResultRaw, graphInstance); + }, [graphSpaceInfo, handleExpand]); + + const handleClickGraphEdge = useCallback( + value => { + const drawerInfo = value.getModel(); + setShowEditElement(true); + setEditElementInfo(drawerInfo); + }, [] + ); + + const handledbClickNode = useCallback( + (node, graphInstance) => { + const model = node.getModel(); + const params = {vertex_id: model.id, vertex_label: model.itemType}; + getExpandInfo(params, graphInstance); + }, + [getExpandInfo] + ); + + const handleAddNode = useCallback( + data => { + const newItem = handleAddGraphNode(data, metaData, styleConfigData, graph); + const {nodes, edges} = graphData; + setGraphData({edges, nodes: [...nodes, newItem]}); + setGraphAllInfo({...graphAllInfo, vertexCount: Number(graphAllInfo.vertexCount) + 1}); + }, + [graph, graphAllInfo, graphData, metaData, styleConfigData] + ); + + const handleAddEdge = useCallback( + data => { + const newGraphData = handleAddGraphEdge(data, metaData, graphData, styleConfigData, graph); + setGraphData(newGraphData); + setGraphAllInfo({...graphAllInfo, edgeCount: Number(graphAllInfo.edgeCount) + 1}); + }, + [graph, graphAllInfo, graphData, metaData, styleConfigData] + ); + + const toggleAddNodeDrawer = useCallback( + () => { + setShowAddNodeDrawer(pre => !pre); + }, [] + ); + + const toggleAddEdgeDrawer = useCallback( + () => { + setShowAddEdgeDrawer(pre => !pre); + }, [] + ); + + const handleClosePanel = useCallback( + () => { + updatePanelType(CLOSED); + }, + [CLOSED, updatePanelType] + ); + + const handleRedoUndoChange = useCallback( + (type, values) => { + let changedData; + if (type === 'changedata') { + changedData = values; + } + else { + changedData = graph.cfg.data; + } + setGraphData({...changedData}); + }, + [graph] + ); + + const handleClearGraph = useCallback( + () => { + resetGraphStatus && resetGraphStatus(STANDBY, undefined, {}); + updatePanelType(CLOSED); + }, + [CLOSED, STANDBY, resetGraphStatus, updatePanelType] + ); + + const handleClickAddNode = useCallback(() => { + setShowAddNodeDrawer(true); + }, []); + + const handleClickAddEdge = useCallback( + (info, isOutEdge) => { + setIsClickNew(false); + setShowAddEdgeDrawer(true); + setAddEdgeDrawerInfo(info); + setOutEdge(isOutEdge); + }, + [] + ); + + const handleClickMenuExpand = useCallback( + params => { + getExpandInfo(params, graph); + }, + [getExpandInfo, graph] + ); + + const handleSearch = useCallback( + vertex => { + setSearchVisible(true); + setSearchVertex(vertex); + }, + [] + ); + + const onCloseEditElement = useCallback( + () => { + setShowEditElement(false); + }, + [] + ); + + const onEditElementChange = useCallback( + (type, item, itemData) => { + const {id} = item.getModel(); + const updatedInfo = graphData[type].map( + item => { + if (item.id === id) { + return {...item, ...itemData}; + } + return item; + } + ); + const updatedGraphData = {...graphData, [type]: updatedInfo}; + setGraphData(updatedGraphData); + }, + [graphData] + ); + + const handleCloseSearch = useCallback( + () => { + setSearchVisible(false); + }, + [] + ); + + const handleChangeSearch = useCallback( + params => { + getExpandInfo(params, graph); + handleCloseSearch(); + }, + [getExpandInfo, graph, handleCloseSearch] + ); + const renderCanvas2D = () => ( + + + + + + + + + + + + + + + + ); + + const renderCanvas3D = () => (); + + const statusMessage = useMemo(() => ({ + [STANDBY]: t('analysis.query_result.no_data'), + [LOADING]: t('analysis.query_result.loading'), + [FAILED]: queryMessage || t('analysis.query_result.run_failed'), + [UPLOAD_FAILED]: queryMessage || t('analysis.query_result.import_failed'), + }), [LOADING, FAILED, STANDBY, UPLOAD_FAILED, queryMessage, t]); + + const renderMainContent = () => { + if (queryStatus === SUCCESS) { + if (!isQueryMode) { + return ( + + ); + } + if (!showCanvasInfo && !noneGraphAlgorithm.includes(canonicalAlgorithmName)) { + return ( + + ); + } + switch (canonicalAlgorithmName) { + case JACCARD_SIMILARITY: + case ADAMIC_ADAR: + case RESOURCE_ALLOCATION: + return ; + case JACCARD_SIMILARITY_POST: + case RANK_API: + return ; + case NEIGHBOR_RANK_API: + return ; + }; + return graphRenderMode === CANVAS2D ? renderCanvas2D() : renderCanvas3D(); + } + return ; + }; + + const handleSwitchRenderMode = useCallback( + value => { + onGraphRenderModeChange(value); + updatePanelType(CLOSED); + }, + [CLOSED, onGraphRenderModeChange, updatePanelType] + ); + + return ( +
    + + {renderMainContent()} + +
    + ); +}; + +export default GraphResult; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.module.scss new file mode 100644 index 000000000..90197732f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.module.scss @@ -0,0 +1,29 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.graphResult { + position: relative; + width: calc(100% - 250px); + background-color: #fff; + + .graphContainer { + display: flex; + justify-content: flex-start; + flex: 1 1 0; + } +} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.js new file mode 100644 index 000000000..96b20abf2 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.js @@ -0,0 +1,42 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file JACCARD_SIMILARITY等算法展示 + */ + +import React from 'react'; +import {useTranslation} from 'react-i18next'; +import JaccRankView from '../../../component/JaccRankView'; +import c from './index.module.scss'; + +const JaccView = props => { + const {t} = useTranslation(); + const {jaccardsimilarity} = props; + return ( +
    + +
    + ); + +}; + +export default JaccView; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.module.scss new file mode 100644 index 000000000..265afee53 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.module.scss @@ -0,0 +1,31 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.noneGraphContent{ + display: flex; + padding-top: 10%; + padding-bottom: 100px; + align-items: center; + flex-direction: column; + width: 100%; + height: calc(100vh - 40px); + border: 1px solid #E7E8E9; + margin-left: -1px; + word-break: break-all; + overflow-y: auto; +} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.js new file mode 100644 index 000000000..db0bf2cab --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.js @@ -0,0 +1,70 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file NeighborRankApi算法展示 + */ + +import React from 'react'; +import {useTranslation} from 'react-i18next'; +import {colors} from '../../../../utils/constants'; +import JaccRankView from '../../../component/JaccRankView'; +import _ from 'lodash'; +import c from './index.module.scss'; + +const NeighborRankApiView = props => { + const {t} = useTranslation(); + const {rankArray} = props; + const colorsNum = colors.length; + return ( +
    + {rankArray.map((item, index) => { + return ( +
    +
    + {t('analysis.algorithm.result.category', {index: index + 1})} +
    + {_.isEmpty(item) ? ( +
    + {t('analysis.algorithm.result.no_neighbor_at_degree', { + index: index + 1, + })} +
    + ) : (Object.entries(item)?.map(item2 => { + const [key, value] = item2; + return ( + + ); + }) + )} +
    + ); + })} +
    + ); +}; + +export default NeighborRankApiView; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.module.scss new file mode 100644 index 000000000..f852dd6b9 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.module.scss @@ -0,0 +1,43 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.noneGraphContent{ + display: flex; + padding-top: 10%; + padding-bottom: 100px; + align-items: center; + flex-direction: column; + width: 100%; + height: calc(100vh - 40px); + border: 1px solid #E7E8E9; + margin-left: -1px; + word-break: break-all; + overflow-y: auto; + + .noneGraphContentTitle{ + font-weight: bold; + margin-top: 20px; + } + + .emptyDesc { + display: flex; + align-items: center; + margin-top: 20px; + width: 380px + } +} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js new file mode 100644 index 000000000..20da0bb46 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js @@ -0,0 +1,67 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file RankApi算法展示 + */ + +import React from 'react'; +import {useTranslation} from 'react-i18next'; +import {GRAPH_STATUS} from '../../../../utils/constants'; +import JaccRankView from '../../../component/JaccRankView'; +import GraphStatusView from '../../../component/GraphStatusView'; +import _ from 'lodash'; +import c from './index.module.scss'; + +const RankApiView = props => { + const {t} = useTranslation(); + const {rankObj} = props; + if (_.isEmpty(rankObj)) { + return ( + + ); + } + return ( +
    + { + Object.entries(rankObj)?.map( + item => { + const [key, value] = item; + return ( + + ); + } + ) + } +
    + ); + +}; + +export default RankApiView; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.module.scss new file mode 100644 index 000000000..265afee53 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.module.scss @@ -0,0 +1,31 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.noneGraphContent{ + display: flex; + padding-top: 10%; + padding-bottom: 100px; + align-items: center; + flex-direction: column; + width: 100%; + height: calc(100vh - 40px); + border: 1px solid #E7E8E9; + margin-left: -1px; + word-break: break-all; + overflow-y: auto; +} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/utils/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/utils/index.js new file mode 100644 index 000000000..ec4cff6d8 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/utils/index.js @@ -0,0 +1,135 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import * as api from '../../../../api/index'; +import {message} from 'antd'; +import i18n from '../../../../i18n'; +import {formatToDownloadData, formatToGraphData, + formatToOptionedGraphData} from '../../../../utils/formatGraphResultData'; +import {processParallelEdges} from '../../../../utils/graph'; +import {clearSelectedStates} from '../../../../utils/handleGraphState'; +import _ from 'lodash'; + +const fetchExpandInfo = async (params, graphInstance, graphSpaceInfo) => { + const {graphSpace, graph} = graphSpaceInfo; + const response = await api.analysis.putExecutionQuery(graphSpace, graph, params); + if (response.status !== 200) { + message.error(i18n.t('analysis.query_result.expand_failed')); + return; + } + const {vertices, edges} = response.data.graph_view; + if (vertices.length === 0) { + message.warning(i18n.t('analysis.query_result.no_more_neighbors')); + return; + } + const tmp = {vertices: [], edges: []}; + const rawGraphData = formatToDownloadData(graphInstance.save()); + const verticesIds = rawGraphData.vertices.map(item => item.id); + const edgesIds = rawGraphData.edges.map(item => item.id); + for (let item of vertices) { + if (!verticesIds.includes(item.id)) { + tmp.vertices.push(item); + } + } + for (let item of edges) { + if (!edgesIds.includes(item.id)) { + tmp.edges.push(item); + } + } + if (tmp.vertices.length === 0) { + message.warning(i18n.t('analysis.query_result.no_more_neighbors')); + return; + } + const searchResultRaw = tmp; + return searchResultRaw; +}; + +const handleAddGraphNode = (data, metaData, styleConfigData, graph) => { + const addItem = data.vertices; + const {id} = addItem[0]; + const newStyledData = formatToGraphData(data, metaData, styleConfigData); + const styledItem = newStyledData.nodes[0]; + graph.addItem('node', styledItem, false); + const {layout} = graph.cfg; + if (layout) { + graph.destroyLayout(); + graph.updateLayout(layout); + graph.refresh(); + } + clearSelectedStates(graph); + const instance = graph.findById(id); + graph.setItemState(instance, 'addActive', true); + return styledItem; +}; + +const handleAddGraphEdge = (data, metaData, graphData, styleConfigData, graph) => { + const addItem = data.edges; + const {id} = addItem[0]; + const {nodes, edges} = graphData; + const newStyledData = formatToGraphData(data, metaData, styleConfigData); + const processedEdges = processParallelEdges([...edges, newStyledData.edges[0]]); + const styledItem = _.find(processedEdges, {id: id}); + graph.addItem('edge', styledItem, false); + const newGraphData = {edges: processedEdges, nodes}; + clearSelectedStates(graph); + const instance = graph.findById(id); + graph.setItemState(instance, 'addActive', true); + return newGraphData; +}; + +const handleExpandGraph = (newData, metaData, styleConfigData, options, algorithmName, graphInstance) => { + const newRawGraphData = formatToGraphData(newData, metaData, styleConfigData); + const newGraphData = formatToOptionedGraphData(newRawGraphData, options, algorithmName); + const saveData = graphInstance.save(); + const nodes = []; + const edges = []; + const saveNodeIds = saveData.nodes.map(item => { + const {id, icon, label, labelCfg, itemType, metaConfig, properties, size, style, comboId} = item; + nodes.push({id, icon, label, labelCfg, + itemType, metaConfig, properties, size, style, legendType: itemType, comboId}); + return id; + }); + const saveEdgesIds = saveData.edges.map(item => { + const {id, label, rawLabel, labelCfg, itemType, loopCfg, metaConfig, properties, + source, stateStyles, style, target, type} = item; + edges.push({id, label, rawLabel, labelCfg, itemType, loopCfg, metaConfig, properties, + source, stateStyles, style, target, type, legendType: itemType}); + return id; + }); + newGraphData.nodes.forEach(item => { + if (!saveNodeIds.includes(item.id)) { + nodes.push(item); + } + }); + newGraphData.edges.forEach(item => { + if (!saveEdgesIds.includes(item.id)) { + edges.push(item); + } + }); + graphInstance.changeData({nodes, edges, combos: saveData?.combos}, true); + graphInstance.refresh(); + return {nodes, edges, combos: saveData?.combos}; +}; + + +export { + fetchExpandInfo, + handleAddGraphNode, + handleAddGraphEdge, + handleExpandGraph, +}; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/utils/index.test.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/utils/index.test.js new file mode 100644 index 000000000..a927cde69 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/utils/index.test.js @@ -0,0 +1,73 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +jest.mock('@antv/graphin-icons', () => () => ({glyphs: []})); +jest.mock('antd', () => ({message: {error: jest.fn(), warning: jest.fn()}})); +jest.mock('../../../../api/index', () => ({ + analysis: { + putExecutionQuery: jest.fn(), + }, +})); + +import {handleExpandGraph} from './index'; + +const metaData = { + vertexMeta: [{name: 'person', style: {display_fields: ['~id']}}], + edgeMeta: [{name: 'knows', style: {display_fields: ['~id']}}], +}; + +test('does not append duplicate nodes or edges while expanding algorithm result', () => { + const graphInstance = { + save: () => ({ + nodes: [{ + id: 'v1', + label: 'v1', + itemType: 'person', + legendType: 'person', + properties: {}, + }], + edges: [{ + id: 'e1', + label: 'e1', + rawLabel: 'knows', + itemType: 'knows', + legendType: 'knows', + source: 'v1', + target: 'v1', + properties: {}, + }], + combos: [], + }), + changeData: jest.fn(), + refresh: jest.fn(), + }; + + const result = handleExpandGraph({ + vertices: [{id: 'v1', label: 'person', properties: {}}], + edges: [{id: 'e1', label: 'knows', source: 'v1', target: 'v1', properties: {}}], + }, metaData, {}, {}, undefined, graphInstance); + + expect(result.nodes).toHaveLength(1); + expect(result.edges).toHaveLength(1); + expect(result.combos).toEqual([]); + expect(graphInstance.changeData).toHaveBeenCalledWith({ + nodes: result.nodes, + edges: result.edges, + combos: [], + }, true); +}); diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.js new file mode 100644 index 000000000..582d2d9ea --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.js @@ -0,0 +1,318 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图算法 Home + */ + +import React, {useCallback, useState, useEffect, useContext} from 'react'; +import AlgorithmFormHome from '../algorithmsForm/Home'; +import GraphResult from '../GraphResult/Home'; +import LogsDetail from '../LogsDetail/Home'; +import GraphAnalysisContext from '../../Context'; +import {GRAPH_STATUS, PANEL_TYPE, GRAPH_RENDER_MODE, useTranslatedConstants} from '../../../utils/constants'; +import * as api from '../../../api'; +import _ from 'lodash'; +import c from './index.module.scss'; + + + +const AlgorithmHome = () => { + const {ALGORITHM_MODE} = useTranslatedConstants(); + + const {STANDBY} = GRAPH_STATUS; + const {CLOSED} = PANEL_TYPE; + const {OLTP, OLAP} = ALGORITHM_MODE; + const {CANVAS2D} = GRAPH_RENDER_MODE; + const defaultPageParams = {page: 1, pageSize: 10}; + const TYPE = {GREMLIN: '0', ALGORITHM: '1', CYPHER: '2'}; + + const {graphSpace, graph} = useContext(GraphAnalysisContext); + + const [metaData, setMetaData] = useState(); + const [propertyKeysRecords, setPropertyKeysRecords] = useState(); + const [graphNums, setGraphNums] = useState({vertexCount: -1, edgeCount: -1}); + const [algorithmMode, setAlgorithmMode] = useState(); + const [queryStatus, setQueryStatus] = useState(STANDBY); + const [queryMessage, setQueryMessage] = useState(); + const [queryResult, setQueryResult] = useState(); + const [asyncTaskResult, setAsyncTaskResult] = useState(); + const [graphOptions, setGraphOptions] = useState(); + const [panelType, setPanelType] = useState(CLOSED); + const [algorithmOnCanvas, setAlgorithmOnCanvas] = useState(); + const [pageExecute, setExecutePage] = useState(defaultPageParams.page); + const [pageFavorite, setFavoritePage] = useState(defaultPageParams.page); + const [pageSize, setPageSize] = useState(defaultPageParams.pageSize); + const [isLoading, setLoading] = useState(false); + const [favorSearch, setFavorSearch] = useState(); + const [sortMode, setSortMode] = useState(); + const [favoriteQueriesData, setFavoriteQueriesData] = useState({}); + const [executionLogsData, setExecutionLogsData] = useState({}); + const [graphRenderMode, setGraphRenderMode] = useState(CANVAS2D); + + const initQueryResult = useCallback( + () => { + setQueryStatus(STANDBY); + setQueryMessage(); + setQueryResult({}); + setPanelType(CLOSED); + }, + [CLOSED, STANDBY] + ); + + const getMetaData = useCallback( + async () => { + let edgeMeta; + let vertexMeta; + const edgeMetaResponse = await api.manage.getMetaEdgeList(graphSpace, graph, {page_size: -1}); + if (edgeMetaResponse.status === 200) { + edgeMeta = edgeMetaResponse.data.records; + } + const vertexMetaResponse = await api.manage.getMetaVertexList(graphSpace, graph, {page_size: -1}); + if (vertexMetaResponse.status === 200) { + vertexMeta = vertexMetaResponse.data.records; + } + setMetaData({edgeMeta, vertexMeta}); + }, + [graph, graphSpace] + ); + + const getPropertykeys = useCallback( + async () => { + const response = await api.manage.getMetaPropertyList(graphSpace, graph, {page_size: -1}); + if (response.status === 200) { + setPropertyKeysRecords(response?.data?.records ?? []); + } + }, + [graph, graphSpace] + ); + + const getGraphNumsInfo = useCallback( + async () => { + const response = await api.analysis.getGraphData(graphSpace, graph); + const {status, data} = response || {}; + if (status === 200) { + const {vertexcount, edgecount} = data || {}; + setGraphNums({vertexCount: vertexcount, edgeCount: edgecount}); + } + }, + [graph, graphSpace] + ); + + const getFavoriteQueriesList = useCallback( + async () => { + const params = { + 'page_size': pageSize, + 'page_no': pageFavorite, + 'content': favorSearch, + 'time_order': sortMode, + 'type': 'ALGORITHM', + }; + setLoading(true); + const response = await api.analysis.fetchFavoriteQueries(graphSpace, graph, params); + setLoading(false); + const {status, data} = response || {}; + if (status !== 200) { + setFavoriteQueriesData({records: [], total: 0}); + } + else { + setFavoriteQueriesData({records: data?.records ?? [], total: data?.total ?? 0}); + } + }, + [favorSearch, graph, graphSpace, pageFavorite, pageSize, sortMode] + ); + + const getExecutionLogsList = useCallback( + async () => { + const params = {'page_size': pageSize, 'page_no': pageExecute, 'type': TYPE.ALGORITHM}; + setLoading(true); + const response = await api.analysis.getExecutionLogs(graphSpace, graph, params); + setLoading(false); + const {status, data} = response || {}; + if (status !== 200) { + setExecutionLogsData({records: [], total: 0}); + } + else { + setExecutionLogsData({records: data?.records ?? [], total: data?.total ?? 0}); + } + }, + [TYPE.ALGORITHM, graph, graphSpace, pageExecute, pageSize] + ); + + const onFavoriteRefresh = useCallback(() => { + getFavoriteQueriesList(); + }, [getFavoriteQueriesList]); + + useEffect( + () => { + getExecutionLogsList(); + getFavoriteQueriesList(); + }, + [getExecutionLogsList, getFavoriteQueriesList] + ); + + useEffect( + () => { + if (pageFavorite > 1 && _.isEmpty(favoriteQueriesData.records)) { + setFavoritePage(pageFavorite - 1); + } + }, + [favoriteQueriesData, pageFavorite, pageSize] + ); + + const resetGraphInfo = useCallback( + () => { + getMetaData(); + getPropertykeys(); + getGraphNumsInfo(); + }, + [getGraphNumsInfo, getMetaData, getPropertykeys] + ); + + const onResetPage = useCallback( + () => { + setExecutePage(defaultPageParams.page); + setFavoritePage(defaultPageParams.page); + }, [defaultPageParams.page] + ); + + useEffect(() => { + if (graphSpace && graph) { + resetGraphInfo(); + } + initQueryResult(); + onResetPage(); + }, [graph, graphSpace, initQueryResult, onResetPage, resetGraphInfo]); + + + const handleUpdateCurrentAlgorithm = useCallback(value => { + setAlgorithmOnCanvas(value); + }, []); + + const handleOltpFormSubmit = useCallback( + (status, data, message, options) => { + setPanelType(CLOSED); + setGraphRenderMode(CANVAS2D); + setAlgorithmMode(OLTP); + setAsyncTaskResult(); + setQueryStatus(status); + setQueryResult(data); + setQueryMessage(message); + options && setGraphOptions(options); + getExecutionLogsList(); + }, + [CANVAS2D, CLOSED, OLTP, getExecutionLogsList] + ); + + const handleOlapFormSubmit = useCallback( + (status, data, message) => { + setPanelType(CLOSED); + setAlgorithmMode(OLAP); + setQueryResult({}); + setQueryStatus(status); + setAsyncTaskResult(data); + setQueryMessage(message || ''); + }, + [CLOSED, OLAP] + ); + + const resetGraphStatus = useCallback( + (status, message, data) => { + setPanelType(CLOSED); + setAlgorithmMode(OLTP); + setAlgorithmOnCanvas(); + setGraphOptions(); + status && setQueryStatus(status); + message && setQueryMessage(message); + data && setQueryResult(data); + }, [CLOSED, OLTP]); + + const updatePanelType = useCallback(type => { + setPanelType(type); + }, []); + + const onExecutePageChange = useCallback((page, pageSize) => { + setExecutePage(page); + setPageSize(pageSize); + }, []); + + const onFavoritePageChange = useCallback((page, pageSize) => { + setFavoritePage(page); + setPageSize(pageSize); + }, []); + + const onChangeFavorSearch = useCallback(values => { + setFavorSearch(values); + }, []); + + const onSortChange = useCallback((pagination, filters, sort) => { + setSortMode(sort.order === 'ascend' ? 'asc' : 'desc'); + }, []); + + const onGraphRenderModeChange = useCallback( + value => { + setGraphRenderMode(value); + }, + [] + ); + + return ( + <> +
    + + +
    + + + ); +}; + +export default AlgorithmHome; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.module.scss new file mode 100644 index 000000000..8ee5b304a --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.module.scss @@ -0,0 +1,24 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.algorithmContent { + display: flex; + flex-direction: row; + margin-top: 10px; + height: calc(100vh - 1px); +} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.js new file mode 100644 index 000000000..310309e02 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.js @@ -0,0 +1,204 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图算法 执行记录 + */ + +import React, {useState, useCallback} from 'react'; +import {useTranslation} from 'react-i18next'; +import {Button, Table, Space, Tag, Input, Popconfirm} from 'antd'; +import ExecutionContent from '../../../../components/ExecutionContent'; +import c from './index.module.scss'; + +const EXECUTE_TYPE_KEY = { + GREMLIN: 'GREMLIN', + GREMLIN_ASYNC: 'GREMLIN_ASYNC', + ALGORITHM: 'ALGORITHM', + CYPHER: 'CYPHER', +}; + +const EXECUTE_STATUS_KEY = { + SUCCESS: 'SUCCESS', + ASYNC_TASK_SUCCESS: 'ASYNC_TASK_SUCCESS', + ASYNC_TASK_RUNNING: 'ASYNC_TASK_RUNNING', + RUNNING: 'RUNNING', + FAILED: 'FAILED', + ASYNC_TASK_FAILED: 'ASYNC_TASK_FAILED', +}; + +const statusColor = { + SUCCESS: 'green', + ASYNC_TASK_SUCCESS: 'green', + ASYNC_TASK_RUNNING: 'geekblue', + RUNNING: 'geekblue', + FAILED: 'volcano', + ASYNC_TASK_FAILED: 'volcano', +}; + +const createValueHandler = (handler, value) => () => handler(value); +const getRowKey = item => item.id; + +const ExecuteLog = props => { + const {t} = useTranslation(); + const { + isLoading, + pageExecute, + pageSize, + onExecutePageChange, + onAddCollection, + executionLogsDataRecords, + executionLogsDataTotal, + } = props; + + const [favoriteName, setFavoriteName] = useState(); + const [disabledFavorite, setDisabledFavorite] = useState(true); + + const onFavoraiteName = useCallback( + e => { + setFavoriteName(e.target.value); + e.target.value ? setDisabledFavorite(false) : setDisabledFavorite(true); + }, []); + + const onFavoriteCard = useCallback(() => { + setFavoriteName(''); + setDisabledFavorite(true); + }, []); + + const updateAddCollection = useCallback( + content => { + onAddCollection(content, favoriteName); + }, + [favoriteName, onAddCollection] + ); + + const onAddFavorite = useCallback( + content => { + updateAddCollection(content); + }, [updateAddCollection]); + + const favoriteContent = rowData => ( + <> +
    + {t('analysis.logs.favorite_statement')} +
    + + + ); + + const typeDesc = type => { + const typeKey = EXECUTE_TYPE_KEY[type]; + return typeKey ? t(`analysis.logs.type.${typeKey}`) : type; + }; + + const statusDesc = status => { + const statusKey = EXECUTE_STATUS_KEY[status]; + return statusKey ? t(`analysis.logs.status.${statusKey}`) : status; + }; + + const executeLogColumns = [ + { + title: t('analysis.logs.column.time'), + dataIndex: 'create_time', + width: '20%', + }, + { + title: t('analysis.logs.column.type'), + dataIndex: 'type', + width: '15%', + render: type => typeDesc(type), + }, + { + title: t('analysis.logs.column.content'), + dataIndex: 'content', + width: '30%', + render: (text, rowData, index) => { + return text.split('\n')[1] ? + :
    {text}
    ; + }, + }, + { + title: t('analysis.logs.column.status'), + dataIndex: 'status', + width: '10%', + render: status => { + return ( + + + {statusDesc(status)} + + + ); + }, + }, + { + title: t('analysis.logs.column.duration'), + dataIndex: 'duration', + width: '10%', + }, + { + title: t('analysis.logs.column.action'), + dataIndex: 'manipulation', + width: '15%', + render: (text, rowData, index) => { + return ( +
    + + + +
    + ); + }, + }, + ]; + + return ( +
    10, + current: pageExecute, + pageSize: pageSize, + }} + loading={isLoading} + /> + ); +}; + +export default ExecuteLog; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.module.scss new file mode 100644 index 000000000..e33f1e9d1 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.module.scss @@ -0,0 +1,24 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + + +.breakWord { + cursor: pointer; + word-wrap: break-word; + word-break: break-word; +} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.js new file mode 100644 index 000000000..dbbbe8aeb --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.js @@ -0,0 +1,239 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图算法 收藏 + */ + +import React, {useState, useCallback} from 'react'; +import {useTranslation} from 'react-i18next'; +import Highlighter from 'react-highlight-words'; +import {Button, Table, Input, Popconfirm, Modal} from 'antd'; +import ExecutionContent from '../../../../components/ExecutionContent'; +import c from './index.module.scss'; + +const createValueHandler = (handler, value) => () => handler(value); +const getRowKey = item => item.id; + +const Favorite = props => { + const {t} = useTranslation(); + const { + isLoading, + pageFavorite, + pageSize, + onFavoritePageChange, + onChangeFavorSearch, + onSortChange, + onEditCollection, + onDel, + favoriteQueriesDataRecords, + favoriteQueriesDataTotal, + } = props; + + const [favoriteName, setFavoriteName] = useState(); + const [searchCache, setSearchCache] = useState(''); + const [search, setSearch] = useState(''); + const [isDisabledName, setDisabledName] = useState(false); + + const changeCollection = useCallback( + rowData => { + onEditCollection(rowData, favoriteName); + }, + [favoriteName, onEditCollection] + ); + + const onSaveEditFavorite = useCallback( + rowData => { + setFavoriteName(''); + changeCollection(rowData); + }, [changeCollection]); + + const onEditFavorite = useCallback( + rowData => { + const {name} = rowData; + setFavoriteName(name); + }, []); + + const onChangeFavoraiteName = useCallback( + e => { + setFavoriteName(e.target.value); + e.target.value ? setDisabledName(false) : setDisabledName(true); + }, []); + + const onConfirm = id => { + Modal.confirm({ + title: t('analysis.logs.confirm_delete'), + content: t('analysis.logs.delete_favorite_confirm'), + okText: t('common.action.confirm'), + cancelText: t('common.action.cancel'), + onOk: () => onDel(id), + }); + }; + + const editFavoriteForm = ( + <> +
    + {t('analysis.logs.edit_name')} +
    + + + ); + + const queryFavoriteColumns = [ + { + title: t('analysis.logs.column.time'), + dataIndex: 'create_time', + width: '25%', + sorter: true, + }, + { + title: t('analysis.logs.column.name'), + dataIndex: 'name', + width: '15%', + sorter: true, + render: text => { + return ( + + ); + }, + }, + { + title: t('analysis.logs.column.favorite_statement'), + dataIndex: 'content', + width: '40%', + render(text, rowData) { + return text.split('\n')[1] ? ( + + ) : ( +
    + +
    + ); + }, + }, + { + title: t('analysis.logs.column.action'), + dataIndex: 'manipulation', + width: '20%', + render(_, rowData, index) { + return ( +
    + + + + +
    + ); + }, + }, + ]; + + const onSearchChange = useCallback( + e => { + const value = e.target.value; + setSearchCache(value); + if (!value) { + setSearch(value); + } + onChangeFavorSearch(value); + }, + [onChangeFavorSearch] + ); + + const onSearch = useCallback( + () => { + if (searchCache !== search) { + setSearch(searchCache); + } + onChangeFavorSearch(searchCache); + }, + [search, searchCache, onChangeFavorSearch] + ); + + return ( + <> +
    + +
    +
    10, + current: pageFavorite, + pageSize: pageSize, + }} + loading={isLoading} + /> + + ); +}; + +export default Favorite; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.module.scss new file mode 100644 index 000000000..5a2c0c15f --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.module.scss @@ -0,0 +1,40 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + + +.searchFavorite { + float: right; + margin:16px 20px; +} + +.breakWord { + cursor: pointer; + word-wrap: break-word; + word-break: break-word; +} + +.highlight { + color: #1890ff; + background-color: #fff; +} + +.searchBar { + margin-bottom: 16px; + text-align: right; +} + diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Home/index.js new file mode 100644 index 000000000..c7b571a9b --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Home/index.js @@ -0,0 +1,178 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图算法表格 Home + */ + +import React, {useCallback, useContext} from 'react'; +import {useTranslation} from 'react-i18next'; +import GraphAnalysisContext from '../../../Context'; +import {Tabs, message} from 'antd'; +import ExecuteLog from '../ExecuteLog'; +import Favorite from '../Favorite'; +import * as api from '../../../../api/index'; +import c from './index.module.scss'; + +const LogsDetail = props => { + const {t} = useTranslation(); + const { + isLoading, + pageExecute, + pageFavorite, + pageSize, + onExecutePageChange, + onFavoritePageChange, + onChangeFavorSearch, + onSortChange, + onRefresh, + favoriteQueriesData, + executionLogsData, + } = props; + + const {graphSpace: currentGraphSpace, graph: currentGraph} = useContext(GraphAnalysisContext); + const {records: favoriteQueriesDataRecords, total: favoriteQueriesDataTotal} = favoriteQueriesData; + const {records: executionLogsDataRecords, total: executionLogsDataTotal} = executionLogsData; + + const addItemByName = useCallback( + (content, favoriteName) => { + const params = { + 'content': content, + 'name': favoriteName, + 'type': 'ALGORITHM', + }; + api.analysis.addFavoriate(currentGraphSpace, currentGraph, params) + .then(res => { + const {status, message: errMsg} = res; + if (status === 200) { + message.success(t('analysis.logs.favorite_success')); + onRefresh(); + } + else { + !errMsg && message.error(t('analysis.logs.favorite_failed')); + } + }).catch(err => { + console.error(err); + }); + }, [currentGraph, currentGraphSpace, onRefresh, t]); + + const onAddHandler = useCallback( + (content, favoriteName) => { + addItemByName(content, favoriteName); + }, + [addItemByName] + ); + + const delItemByRowId = useCallback( + favoriteId => { + api.analysis.deleteQueryCollection(currentGraphSpace, currentGraph, favoriteId) + .then(res => { + const {status, message: errMsg} = res; + if (status === 200) { + message.success(t('analysis.logs.delete_success')); + onRefresh(); + } + else { + !errMsg && message.error(t('analysis.logs.delete_failed')); + } + }).catch(err => { + console.error(err); + }); + }, [currentGraph, currentGraphSpace, onRefresh, t]); + + const onDelHandler = useCallback( + id => { + delItemByRowId(id); + }, + [delItemByRowId] + ); + + const editItemByRow = useCallback( + (rowData, favoriteName) => { + const params = { + id: rowData.id, + content: rowData.content, + name: favoriteName, + 'type': 'ALGORITHM', + }; + api.analysis.editQueryCollection(currentGraphSpace, currentGraph, params) + .then(res => { + const {status, message: errMsg} = res; + if (status === 200) { + message.success(t('analysis.logs.edit_success')); + onRefresh(); + } + else { + !errMsg && message.error(t('analysis.logs.edit_failed')); + } + }).catch(err => { + console.error(err); + }); + }, [currentGraph, currentGraphSpace, onRefresh, t]); + + const onEditHandler = useCallback( + (rowData, favoriteName) => { + editItemByRow(rowData, favoriteName); + }, + [editItemByRow] + ); + + const tabItems = [ + { + label: t('analysis.logs.execute_tab'), + key: 'excutes', + children: ( + + ), + }, + { + label: t('analysis.logs.favorite_tab'), + key: 'favorites', + children: ( + + ), + }, + ]; + + return ( +
    + +
    + ); +}; + +export default LogsDetail; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Home/index.module.scss new file mode 100644 index 000000000..b004b8884 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Home/index.module.scss @@ -0,0 +1,46 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +.footerTabs { + background-color: #fff; + margin-top: 10px; + + :global { + .ant-table-wrapper { + word-break: break-all; + } + + .ant-tabs-nav { + background-color: #f0f2f5; + } + + .ant-tabs-card .ant-tabs-nav .ant-tabs-tab { + background: #f0f2f5; + border: 0; + } + + .ant-tabs-card .ant-tabs-nav .ant-tabs-tab-active { + background: #fff; + } + + .ant-tabs-content-holder{ + padding: 16px; + } + } +} + diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/AlgorithmNameHeader/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/AlgorithmNameHeader/index.js new file mode 100644 index 000000000..0bc5d6c5c --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/AlgorithmNameHeader/index.js @@ -0,0 +1,160 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +/** + * @file 图分析组件 算法标题 + */ + +import React from 'react'; +import Highlighter from 'react-highlight-words'; +import {Typography, Tooltip, Button} from 'antd'; +import c from './index.module.scss'; +import classnames from 'classnames'; +import {useTranslation} from 'react-i18next'; +import { + getAlgorithmDisplayName, + isAlgorithmNameMatched, +} from '../../../../utils/constants'; + +const {Text} = Typography; + +import { + QuestionCircleOutlined, + CaretRightOutlined, +} from '@ant-design/icons'; + +const AlgorithmNameHeader = props => { + const {t} = useTranslation(); + const { + icon, + name, + searchValue, + description, + isRunning, + isDisabled, + handleRunning, + highlightName, + } = props; + + const iconClassName = classnames( + c.panelHeaderIcon, + {[c.panelHeaderIconHighlight]: highlightName} + + ); + const displayName = getAlgorithmDisplayName(name, t); + + const renderAlgorithmName = name => { + let res; + if (isAlgorithmNameMatched(name, searchValue, t)) { + res = ( + + + + ); + } + else { + res = ( + + {displayName} + + ); + } + if (highlightName) { + res = ( + + + + ); + } + return res; + }; + + const renderRunningButton = () => { + if (!isDisabled) { + return ( + {t('analysis.algorithm.run')}} + color={'#fff'} + > +