diff --git a/.github/workflows/bot-auto-cherry-pick.yml b/.github/workflows/bot-auto-cherry-pick.yml index 608d523a0130..ecba3750ffc6 100644 --- a/.github/workflows/bot-auto-cherry-pick.yml +++ b/.github/workflows/bot-auto-cherry-pick.yml @@ -6,7 +6,7 @@ on: jobs: comment: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Comment cherry-pick command uses: actions/github-script@v5 diff --git a/.github/workflows/bot-cherry-pick.yml b/.github/workflows/bot-cherry-pick.yml index 183ee3e251db..670d4f2f8368 100644 --- a/.github/workflows/bot-cherry-pick.yml +++ b/.github/workflows/bot-cherry-pick.yml @@ -6,7 +6,7 @@ jobs: cherry-pick: name: Cherry Pick if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/cherry-pick') && github.event.comment.user.login=='sealos-ci-robot' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Write vars id: set-target @@ -26,7 +26,7 @@ jobs: fi echo "prNumber=$PR_NUMBER" >> $GITHUB_OUTPUT - name: Checkout the latest code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token: ${{ secrets.GH_PAT }} fetch-depth: 0 diff --git a/.github/workflows/bot-issues-translator.yml b/.github/workflows/bot-issues-translator.yml index bc29458d81e9..823fc578d237 100644 --- a/.github/workflows/bot-issues-translator.yml +++ b/.github/workflows/bot-issues-translator.yml @@ -10,7 +10,7 @@ jobs: issues: write discussions: write pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: usthe/issues-translate-action@v2.7 with: diff --git a/.github/workflows/bot-labels.yml b/.github/workflows/bot-labels.yml index ff15f94f4cb0..54d46ed9e2e0 100644 --- a/.github/workflows/bot-labels.yml +++ b/.github/workflows/bot-labels.yml @@ -7,7 +7,7 @@ jobs: permissions: contents: read pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/labeler@v4 with: diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index cee8bc2dbe11..7cd1a037c89e 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -6,10 +6,10 @@ on: jobs: comment: if: startswith(github.event.comment.body, '/') - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout the latest code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Gh Robot for Sealos uses: labring/robot@v2.0.0 with: diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index fa92da48b7b8..23ba7c346f61 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -25,13 +25,13 @@ env: jobs: coverage: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/check-format-code.yml b/.github/workflows/check-format-code.yml index 6215a7e4d02d..9de0b54c75c9 100755 --- a/.github/workflows/check-format-code.yml +++ b/.github/workflows/check-format-code.yml @@ -25,16 +25,16 @@ env: jobs: format-code: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.head_ref }} - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/check-license.yml b/.github/workflows/check-license.yml index 378bf78269cd..fa3a5ffc3f4e 100755 --- a/.github/workflows/check-license.yml +++ b/.github/workflows/check-license.yml @@ -25,15 +25,15 @@ env: jobs: check-license: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/check-link-error.yml b/.github/workflows/check-link-error.yml index c65dd13bd7e5..87887a08df99 100644 --- a/.github/workflows/check-link-error.yml +++ b/.github/workflows/check-link-error.yml @@ -7,9 +7,9 @@ on: jobs: linkChecker: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/check-semgrep.yml b/.github/workflows/check-semgrep.yml index c0a2ab55e223..405e5012e620 100644 --- a/.github/workflows/check-semgrep.yml +++ b/.github/workflows/check-semgrep.yml @@ -25,7 +25,7 @@ jobs: # User-definable name of this GitHub Actions job: name: Scan # If you are self-hosting, change the following `runs-on` value: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 container: # A Docker image with Semgrep installed. Do not change this. @@ -39,7 +39,7 @@ jobs: steps: # Fetch project source with GitHub Actions Checkout. - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Run the "semgrep ci" command on the command line of the docker image. - run: | diff --git a/.github/workflows/ci-patch-image.yml b/.github/workflows/ci-patch-image.yml index 87ec6e05f6ae..f1390f0f4260 100755 --- a/.github/workflows/ci-patch-image.yml +++ b/.github/workflows/ci-patch-image.yml @@ -46,11 +46,11 @@ jobs: container-sealos: needs: [ call_ci_workflow ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: ${{ (github.event_name == 'push') || (inputs.push_mage == true) }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Expose git commit data uses: rlespinasse/git-commit-data-action@v1 - # Add support for more platforms with QEMU (optional) @@ -58,12 +58,12 @@ jobs: name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: driver-opts: network=host - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -110,12 +110,12 @@ jobs: if: ${{ (github.event_name == 'push') || (inputs.push_mage == true) }} needs: - container-sealos - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Expose git commit data uses: rlespinasse/git-commit-data-action@v1 - name: Renew issue and Sync Images @@ -141,12 +141,12 @@ jobs: needs: - call_ci_workflow - save-sealos - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download sealos uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11601ebee4d1..89a3ddf7864f 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,12 +40,12 @@ on: jobs: resolve-modules: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Resolve Modules id: set-matrix @@ -53,15 +53,15 @@ jobs: golangci-lint: needs: [ resolve-modules ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} @@ -69,7 +69,7 @@ jobs: run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev - name: Run Linter - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: version: v1.54.2 working-directory: ${{ matrix.workdir }} diff --git a/.github/workflows/cloud-release.yml b/.github/workflows/cloud-release.yml index c7ab2032aee3..221bf62ead7e 100644 --- a/.github/workflows/cloud-release.yml +++ b/.github/workflows/cloud-release.yml @@ -36,7 +36,7 @@ jobs: release-frontends: if: ${{ github.event.inputs.build_offline_tar_only == false }} - uses: ./.github/workflows/frontend.yml + uses: ./.github/workflows/frontends.yml with: push_image: true push_image_tag: ${{ github.event.inputs.tag || github.event.release.tag_name }} @@ -75,7 +75,7 @@ jobs: OSS_BUCKET: ${{ secrets.OSS_BUCKET }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download sealos uses: actions/download-artifact@v4 with: @@ -114,7 +114,7 @@ jobs: OSS_BUCKET: ${{ secrets.OSS_BUCKET }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download sealos uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/cloud.yml b/.github/workflows/cloud.yml index f244f05a69c0..4fa19378af0c 100644 --- a/.github/workflows/cloud.yml +++ b/.github/workflows/cloud.yml @@ -65,10 +65,10 @@ jobs: if: ${{ (github.event_name == 'release') ||(github.event_name == 'push') || (inputs.push_image == true) }} needs: - save-sealos - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Expose git commit data diff --git a/.github/workflows/controllers.yml b/.github/workflows/controllers.yml index 69ee063c0e6e..cf1597501833 100644 --- a/.github/workflows/controllers.yml +++ b/.github/workflows/controllers.yml @@ -50,12 +50,12 @@ env: LICENSE_KEY: ${{ secrets.LICENSE_KEY }} jobs: resolve-modules: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Resolve Modules id: set-matrix @@ -64,28 +64,28 @@ jobs: golangci-lint: if: ${{ github.event_name }} == 'push' || ${{ github.event_name }} == 'pull_request' needs: [ resolve-modules ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} - name: Install Dependencies run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev - name: Run Linter - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: version: v1.54.2 working-directory: ${{ matrix.workdir }} args: "--out-${NO_FUTURE}format colored-line-number" image-build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: module: @@ -103,12 +103,12 @@ jobs: - { name: objectstorage, path: objectstorage } steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} @@ -150,12 +150,12 @@ jobs: name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: driver-opts: network=host - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: ${{ (github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true) }} with: registry: ghcr.io @@ -163,7 +163,7 @@ jobs: password: ${{ secrets.GH_PAT }} - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository_owner }}/sealos-${{ matrix.module.name }}-controller @@ -172,7 +172,7 @@ jobs: type=raw,value=${{ steps.prepare.outputs.tag_name }},enable=true - name: build (and publish) ${{ matrix.module.name }} main image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: ./controllers/${{ matrix.module.path }} file: ./controllers/${{ matrix.module.path }}/Dockerfile @@ -190,7 +190,7 @@ jobs: needs: - image-build - save-sealos - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: module: @@ -208,7 +208,7 @@ jobs: - { name: objectstorage, path: objectstorage } steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/delete_workflow.yml b/.github/workflows/delete_workflow.yml index 263c2f41aa05..38cab465e511 100644 --- a/.github/workflows/delete_workflow.yml +++ b/.github/workflows/delete_workflow.yml @@ -30,7 +30,7 @@ on: jobs: del_runs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Delete workflow runs uses: Mattraks/delete-workflow-runs@v2 diff --git a/.github/workflows/deploy-docs-preview.yml b/.github/workflows/deploy-docs-preview.yml index d53f2b149dd9..a8465634290d 100644 --- a/.github/workflows/deploy-docs-preview.yml +++ b/.github/workflows/deploy-docs-preview.yml @@ -32,7 +32,7 @@ jobs: name: Preview url: ${{ steps.vercel-action.outputs.preview-url }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # Job outputs outputs: @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -91,9 +91,9 @@ jobs: docsOutput: needs: [ build ] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/deploy-docs-site.yml b/.github/workflows/deploy-docs-site.yml index a2d1cdef61cc..342e85c59ffd 100644 --- a/.github/workflows/deploy-docs-site.yml +++ b/.github/workflows/deploy-docs-site.yml @@ -26,10 +26,10 @@ env: jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Detect package manager id: detect-package-manager diff --git a/.github/workflows/docs-site-image.yml b/.github/workflows/docs-site-image.yml index 8e3bc0199d09..6cb4ef2fb871 100644 --- a/.github/workflows/docs-site-image.yml +++ b/.github/workflows/docs-site-image.yml @@ -9,7 +9,7 @@ on: jobs: build-docs-image: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -53,7 +53,7 @@ jobs: password: ${{ secrets.ALI_HUB_PASSWORD }} - name: Build and push Docker images to ghcr.io and Aliyun Hub - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: ./docs file: ./docs/website/Dockerfile @@ -65,11 +65,11 @@ jobs: update-docs-image: needs: build-docs-image - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.repository == 'labring/sealos' steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions-hub/kubectl@master env: KUBE_CONFIG: ${{ secrets.SEALOS_TOP_KUBE_CONF }} diff --git a/.github/workflows/e2e_k3s_multi_node.yml b/.github/workflows/e2e_k3s_multi_node.yml index da74ac52a9a9..cd7910edded5 100644 --- a/.github/workflows/e2e_k3s_multi_node.yml +++ b/.github/workflows/e2e_k3s_multi_node.yml @@ -19,7 +19,7 @@ jobs: target_pull: true e2e_apply_test: needs: [ call_ci_workflow ] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write strategy: @@ -80,7 +80,7 @@ jobs: echo 'test_${{ matrix.arch }}_result=success' >> "$GITHUB_OUTPUT" issue_commit: needs: [ e2e_apply_test ] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write if: ${{ always() && github.event.label.name == 'need-e2e-apply-test' }} #success() || failure() diff --git a/.github/workflows/e2e_k8s_multi_node.yml b/.github/workflows/e2e_k8s_multi_node.yml index 22bdf1478def..8940d9d85826 100644 --- a/.github/workflows/e2e_k8s_multi_node.yml +++ b/.github/workflows/e2e_k8s_multi_node.yml @@ -19,7 +19,7 @@ jobs: target_pull: true e2e_apply_test: needs: [ call_ci_workflow ] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write strategy: @@ -80,7 +80,7 @@ jobs: echo 'test_${{ matrix.arch }}_result=success' >> "$GITHUB_OUTPUT" issue_commit: needs: [ e2e_apply_test ] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write if: ${{ always() && github.event.label.name == 'need-e2e-apply-test' }} #success() || failure() diff --git a/.github/workflows/e2e_test_core.yml b/.github/workflows/e2e_test_core.yml index d65f056e827f..446c5d5f3777 100644 --- a/.github/workflows/e2e_test_core.yml +++ b/.github/workflows/e2e_test_core.yml @@ -60,7 +60,7 @@ jobs: E2E_sealos_runtime_version_docker_127_test, E2E_sealos_runtime_version_docker_128_test ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Download image-cri-shim uses: actions/download-artifact@v4 diff --git a/.github/workflows/e2e_test_core_k3s.yml b/.github/workflows/e2e_test_core_k3s.yml index 2ef9908dd62a..50769a8522dd 100644 --- a/.github/workflows/e2e_test_core_k3s.yml +++ b/.github/workflows/e2e_test_core_k3s.yml @@ -34,7 +34,7 @@ jobs: [ E2E_sealos_k3s_basic_test, ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Download image-cri-shim uses: actions/download-artifact@v4 diff --git a/.github/workflows/e2e_test_image_cri_shim.yml b/.github/workflows/e2e_test_image_cri_shim.yml index 89c79c698a41..64cf947c8831 100644 --- a/.github/workflows/e2e_test_image_cri_shim.yml +++ b/.github/workflows/e2e_test_image_cri_shim.yml @@ -26,7 +26,7 @@ jobs: image: false verify-image_cri_shim: needs: [ call_ci_workflow ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Install Dependencies run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index a807f20db17f..0456c92b8bf2 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -1,83 +1,131 @@ name: Build Frontend Image on: - create: - tags: workflow_call: inputs: - push_image: - description: 'Push image' - required: false - type: boolean - default: false - push_image_tag: - description: 'Push image tag' - default: 'latest' - required: false + module: + description: "Module" + required: true type: string - workflow_dispatch: - inputs: push_image: - description: 'Push image' + description: "Push image" required: false type: boolean default: false push_image_tag: - description: 'Push image tag' - default: 'latest' + description: "Push image tag" + default: "latest" required: false type: string - push: - branches: ['main'] - paths: - - 'frontend/desktop/**' - - 'frontend/providers/**' - - '.github/workflows/frontend.yml' - - '!**/*.md' - - '!**/*.yaml' - pull_request: - branches: ['*'] - paths: - - 'frontend/desktop/**' - - 'frontend/providers/**' - - '.github/workflows/frontend.yml' - - '!**/*.md' - - '!**/*.yaml' env: # Common versions - GO_VERSION: '1.20' - DEFAULT_OWNER: 'labring' + GO_VERSION: "1.20" + DEFAULT_OWNER: "labring" jobs: image-build: - runs-on: ubuntu-latest strategy: matrix: - module: - [ - providers/license, - providers/cronjob, - providers/template, - providers/adminer, - providers/applaunchpad, - providers/terminal, - providers/dbprovider, - providers/costcenter, - providers/objectstorage, - providers/kubepanel, - providers/workorder, - providers/devbox, - desktop, - ] + include: + - arch: amd64 + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract module name + id: module_name + run: | + MODULE_NAME=$(basename ${{ inputs.module }}) + echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + echo "GHCR_REPO=ghcr.io/${{ github.repository_owner }}/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_REPO }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Hub + if: ${{ inputs.push_image }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GH_PAT }} + + - name: Build + id: build + uses: docker/build-push-action@v6 + with: + context: ./frontend + file: ./frontend/Dockerfile + platforms: linux/${{ matrix.arch }} + build-args: | + name=${{ env.MODULE_NAME }} + path=${{ inputs.module }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,"name=${{ env.GHCR_REPO }}",name-canonical=true,push-by-digest=${{ inputs.push_image }},push=${{ inputs.push_image }} + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.MODULE_NAME }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + image-release: + name: Push Docker Images + needs: image-build + runs-on: ubuntu-24.04 + if: ${{ inputs.push_image }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Login to Github Container Hub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GH_PAT }} + + - name: Extract module name + id: module_name + run: | + MODULE_NAME=$(basename ${{ inputs.module }}) + echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + echo "GHCR_REPO=ghcr.io/${{ github.repository_owner }}/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-${{ env.MODULE_NAME }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Expose git commit data uses: rlespinasse/git-commit-data-action@v1 + - name: Check if tag id: check_tag run: | @@ -86,100 +134,47 @@ jobs: else echo "isTag=false" >> "$GITHUB_OUTPUT" fi + - name: Prepare id: prepare run: | - bash ./scripts/resolve-tag-image.sh "${{ inputs.push_image }}" "${{ steps.check_tag.outputs.isTag }}" "${{ inputs.push_image_tag }}" - - name: Extract module name - id: module_name - run: | - MODULE_NAME=$(basename ${{ matrix.module }}) - echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + bash ./scripts/resolve-tag-image.sh "${{ inputs.push_image }}" "${{ steps.check_tag.outputs.isTag }}" "${{ inputs.push_image_tag }}" - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | - ghcr.io/${{ github.repository_owner }}/sealos-${{ env.MODULE_NAME }}-frontend + ${{ env.GHCR_REPO }} tags: | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} type=raw,value=${{ steps.prepare.outputs.tag_name }},enable=true + type=sha - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.GHCR_REPO }}:${{ steps.meta.outputs.version }} - - name: Login to Github Container Hub - if: ${{ (github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true) }} - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GH_PAT }} - - name: Cache pnpm-store - uses: actions/cache@v3 - with: - path: pnpm-store - key: pnpm-store-${{ hashFiles('Dockerfile') }} - - name: inject pnpm-store into docker - uses: reproducible-containers/buildkit-cache-dance@v2.1.2 - with: - cache-source: pnpm-store - - name: Build And Push - uses: docker/build-push-action@v4 - with: - context: ./frontend - file: ./frontend/Dockerfile - platforms: linux/amd64,linux/arm64 - build-args: | - name=${{ env.MODULE_NAME }} - path=${{ matrix.module }} - # Push if it's a push event or if push_image is true - push: ${{ (github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true) }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - save-sealos: - uses: ./.github/workflows/import-save-sealos.yml - with: - artifact_name: sealos-frontend cluster-image-build: - if: ${{ (github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true) }} needs: - - image-build - - save-sealos - runs-on: ubuntu-latest - strategy: - matrix: - module: - [ - providers/license, - providers/cronjob, - providers/template, - providers/adminer, - providers/applaunchpad, - providers/terminal, - providers/dbprovider, - providers/costcenter, - providers/objectstorage, - providers/kubepanel, - providers/workorder, - providers/devbox, - desktop, - ] + - image-release + if: ${{ inputs.push_image }} + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Extract module name id: module_name run: | - MODULE_NAME=$(basename ${{ matrix.module }}) + MODULE_NAME=$(basename ${{ inputs.module }}) echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV - name: Expose git commit data @@ -195,7 +190,7 @@ jobs: - name: Prepare id: prepare run: | - tag_name=$(bash ./scripts/resolve-tag-image.sh "${{ inputs.push_image }}" "${{ steps.check_tag.outputs.isTag }}" "${{ inputs.push_image_tag }}") + tag_name=$(bash ./scripts/resolve-tag-image.sh "${{ inputs.push_image }}" "${{ steps.check_tag.outputs.isTag }}" "${{ inputs.push_image_tag }}") echo old_docker_repo=ghcr.io/labring/sealos-${{ env.MODULE_NAME }}-frontend >> $GITHUB_OUTPUT echo old_docker_image=ghcr.io/labring/sealos-${{ env.MODULE_NAME }}-frontend:latest >> $GITHUB_OUTPUT echo new_docker_repo=ghcr.io/${{ github.repository_owner }}/sealos-${{ env.MODULE_NAME }}-frontend >> $GITHUB_OUTPUT @@ -220,7 +215,7 @@ jobs: sudo sealos login -u ${{ github.repository_owner }} -p ${{ secrets.GH_PAT }} --debug ghcr.io - name: Build ${{ env.MODULE_NAME }}-frontend cluster image - working-directory: frontend/${{ matrix.module }}/deploy + working-directory: frontend/${{ inputs.module }}/deploy run: | sudo sed -i "s;${{ steps.prepare.outputs.old_docker_image }};${{ steps.prepare.outputs.new_docker_image }};" manifests/* sudo sealos build -t ${{ steps.prepare.outputs.cluster_image }}-amd64 --platform linux/amd64 -f Kubefile @@ -244,14 +239,14 @@ jobs: with: version: v0.0.8-rc1 env: - GH_TOKEN: '${{ secrets.GH_PAT }}' - SEALOS_TYPE: 'issue_renew' - SEALOS_ISSUE_TITLE: '[DaylyReport] Auto build for sealos' - SEALOS_ISSUE_BODYFILE: 'scripts/ISSUE_RENEW.md' - SEALOS_ISSUE_LABEL: 'dayly-report' - SEALOS_ISSUE_TYPE: 'day' - SEALOS_ISSUE_REPO: 'labring-actions/cluster-image' - SEALOS_COMMENT_BODY: '/imagesync ${{ steps.prepare.outputs.cluster_image }}' + GH_TOKEN: "${{ secrets.GH_PAT }}" + SEALOS_TYPE: "issue_renew" + SEALOS_ISSUE_TITLE: "[DaylyReport] Auto build for sealos" + SEALOS_ISSUE_BODYFILE: "scripts/ISSUE_RENEW.md" + SEALOS_ISSUE_LABEL: "dayly-report" + SEALOS_ISSUE_TYPE: "day" + SEALOS_ISSUE_REPO: "labring-actions/cluster-image" + SEALOS_COMMENT_BODY: "/imagesync ${{ steps.prepare.outputs.cluster_image }}" - name: Renew issue and Sync Images for ${{ steps.prepare.outputs.latest_cluster_image }} uses: labring/gh-rebot@v0.0.6 @@ -259,11 +254,11 @@ jobs: with: version: v0.0.8-rc1 env: - GH_TOKEN: '${{ secrets.GH_PAT }}' - SEALOS_TYPE: 'issue_renew' - SEALOS_ISSUE_TITLE: '[DaylyReport] Auto build for sealos' - SEALOS_ISSUE_BODYFILE: 'scripts/ISSUE_RENEW.md' - SEALOS_ISSUE_LABEL: 'dayly-report' - SEALOS_ISSUE_TYPE: 'day' - SEALOS_ISSUE_REPO: 'labring-actions/cluster-image' - SEALOS_COMMENT_BODY: '/imagesync ${{ steps.prepare.outputs.latest_cluster_image }}' + GH_TOKEN: "${{ secrets.GH_PAT }}" + SEALOS_TYPE: "issue_renew" + SEALOS_ISSUE_TITLE: "[DaylyReport] Auto build for sealos" + SEALOS_ISSUE_BODYFILE: "scripts/ISSUE_RENEW.md" + SEALOS_ISSUE_LABEL: "dayly-report" + SEALOS_ISSUE_TYPE: "day" + SEALOS_ISSUE_REPO: "labring-actions/cluster-image" + SEALOS_COMMENT_BODY: "/imagesync ${{ steps.prepare.outputs.latest_cluster_image }}" diff --git a/.github/workflows/frontends.yml b/.github/workflows/frontends.yml new file mode 100644 index 000000000000..d8aed24959fd --- /dev/null +++ b/.github/workflows/frontends.yml @@ -0,0 +1,79 @@ +name: Build Frontends Image + +on: + create: + tags: + workflow_call: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + workflow_dispatch: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + push: + branches: ["main"] + paths: + - "frontend/desktop/**" + - "frontend/providers/**" + - ".github/workflows/frontend.yml" + - "!**/*.md" + - "!**/*.yaml" + pull_request: + branches: ["*"] + paths: + - "frontend/desktop/**" + - "frontend/providers/**" + - ".github/workflows/frontend.yml" + - "!**/*.md" + - "!**/*.yaml" + +jobs: + save-sealos: + uses: ./.github/workflows/import-save-sealos.yml + with: + artifact_name: sealos-frontend + + image-build: + uses: ./.github/workflows/frontend.yml + needs: save-sealos + secrets: inherit + strategy: + fail-fast: false + matrix: + module: + [ + providers/license, + providers/cronjob, + providers/template, + providers/adminer, + providers/applaunchpad, + providers/terminal, + providers/dbprovider, + providers/costcenter, + providers/objectstorage, + providers/kubepanel, + providers/workorder, + providers/devbox, + desktop, + ] + with: + module: ${{ matrix.module }} + push_image: ${{ (github.event_name == 'push') || (github.event_name == 'create') || (inputs.push_image == true) }} + push_image_tag: ${{ inputs.push_image_tag }} diff --git a/.github/workflows/import-patch-image.yml b/.github/workflows/import-patch-image.yml index 13b5ee2b0d83..20de191621a4 100755 --- a/.github/workflows/import-patch-image.yml +++ b/.github/workflows/import-patch-image.yml @@ -26,15 +26,15 @@ on: jobs: resolve-modules-arch: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Checkout if: ${{ (inputs.target_pull == false) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout by Pull Request Target - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: ${{ (inputs.target_pull == true) }} with: ref: ${{ github.event.pull_request.head.ref }} @@ -43,15 +43,15 @@ jobs: id: set-matrix run: bash ./scripts/resolve-patch-modules.sh ${{ inputs.arch }} "true" resolve-modules: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Checkout if: ${{ (inputs.target_pull == false) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout by Pull Request Target - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: ${{ (inputs.target_pull == true) }} with: ref: ${{ github.event.pull_request.head.ref }} @@ -61,23 +61,23 @@ jobs: run: bash ./scripts/resolve-patch-modules.sh ${{ inputs.arch }} build: needs: [ resolve-modules ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} steps: - name: Checkout if: ${{ (inputs.target_pull == false) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Checkout by Pull Request Target - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: ${{ (inputs.target_pull == true) }} with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} @@ -98,22 +98,22 @@ jobs: name: ${{ matrix.binary }}-${{ matrix.arch }} path: bin/linux_${{ matrix.arch}}/${{ matrix.binary }} test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: ${{ (inputs.e2e == true) }} steps: - name: Checkout if: ${{ (inputs.target_pull == false) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Checkout by Pull Request Target - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: ${{ (inputs.target_pull == true) }} with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} - name: Install Dependencies @@ -129,7 +129,7 @@ jobs: path: test/e2e/e2e.test docker: needs: [ resolve-modules-arch,build ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: ${{ (inputs.image == true) }} services: registry: @@ -141,9 +141,9 @@ jobs: steps: - name: Checkout if: ${{ (inputs.target_pull == false) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout by Pull Request Target - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: ${{ (inputs.target_pull == true) }} with: ref: ${{ github.event.pull_request.head.ref }} @@ -176,7 +176,7 @@ jobs: path: docker/sealos - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: driver-opts: network=host @@ -187,7 +187,7 @@ jobs: chmod a+x docker/lvscare/* - name: Build and Push lvscare Image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: docker/lvscare file: docker/lvscare/Dockerfile.main diff --git a/.github/workflows/import-save-sealos.yml b/.github/workflows/import-save-sealos.yml index 2b91cacfb543..5c2aee8e2b6a 100755 --- a/.github/workflows/import-save-sealos.yml +++ b/.github/workflows/import-save-sealos.yml @@ -14,7 +14,7 @@ on: jobs: save-sealos: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Install sealos uses: labring/sealos-action@v0.0.7 diff --git a/.github/workflows/objectstorage.yaml b/.github/workflows/objectstorage.yaml index 6a73be61f262..f27e59fe15a1 100644 --- a/.github/workflows/objectstorage.yaml +++ b/.github/workflows/objectstorage.yaml @@ -67,10 +67,10 @@ jobs: if: ${{ (github.event_name == 'release') ||(github.event_name == 'push') || (inputs.push_image == true) }} needs: - save-sealos - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Expose git commit data diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c5f7ef858ee..c82057c36371 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,15 +14,15 @@ on: jobs: goreleaser: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} @@ -33,14 +33,14 @@ jobs: qemu-user-static binfmt-support - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GH_RELEASE_PAT }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3 + uses: goreleaser/goreleaser-action@v6 with: args: release --clean --release-footer-tmpl=scripts/release/footer.md.tmpl --release-header-tmpl=scripts/release/head.md.tmpl env: @@ -49,12 +49,12 @@ jobs: FURY_TOKEN: ${{ secrets.FURY_TOKEN }} sync: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: - goreleaser steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Fetch Current version @@ -91,12 +91,12 @@ jobs: SEALOS_COMMENT_BODY: "/imagebuild_apps sealos ${{steps.get-current-tag.outputs.tag }}" changelog: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: - goreleaser steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Fetch Current version diff --git a/.github/workflows/services.yml b/.github/workflows/services.yml index 75b7b2850bae..929fd67ac7f4 100644 --- a/.github/workflows/services.yml +++ b/.github/workflows/services.yml @@ -50,12 +50,12 @@ env: jobs: resolve-modules: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Resolve Modules id: set-matrix @@ -63,39 +63,39 @@ jobs: golangci-lint: needs: [ resolve-modules ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} - name: Run Linter - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: version: v1.54.2 working-directory: ${{ matrix.workdir }} args: "--out-${NO_FUTURE}format colored-line-number" image-build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: ## TODO: add more modules - module: [ database, pay, account, minio, launchpad, exceptionmonitor, aiproxy, devbox ] + module: [ database, pay, account, minio, launchpad, exceptionmonitor, aiproxy, devbox, vlogs ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} @@ -138,12 +138,12 @@ jobs: name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: driver-opts: network=host - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: ${{ (github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true) }} with: registry: ghcr.io @@ -152,7 +152,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository_owner }}/sealos-${{ matrix.module }}-service @@ -161,7 +161,7 @@ jobs: type=raw,value=${{ steps.prepare.outputs.tag_name }},enable=true - name: build (and publish) ${{ matrix.module }} main image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: ./service/${{ matrix.module }} file: ./service/${{ matrix.module }}/Dockerfile @@ -180,14 +180,14 @@ jobs: needs: - image-build - save-sealos - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: ## TODO: add more modules - module: [ database, pay, account, minio, launchpad, exceptionmonitor, aiproxy, devbox ] + module: [ database, pay, account, minio, launchpad, exceptionmonitor, aiproxy, devbox, vlogs ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/sync_code.yml b/.github/workflows/sync_code.yml index cb19b9f704fc..1ef254953d27 100644 --- a/.github/workflows/sync_code.yml +++ b/.github/workflows/sync_code.yml @@ -8,10 +8,10 @@ on: workflow_dispatch: jobs: sync: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run GitHub File Sync # Can update to v1 when https://github.com/BetaHuhn/repo-file-sync-action/issues/168 is resolved diff --git a/.github/workflows/sync_docs.yml b/.github/workflows/sync_docs.yml index 9bd054624a8e..71f9300c0a24 100644 --- a/.github/workflows/sync_docs.yml +++ b/.github/workflows/sync_docs.yml @@ -9,10 +9,10 @@ on: workflow_dispatch: jobs: sync: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run GitHub File Sync uses: BetaHuhn/repo-file-sync-action@v1.21.0 diff --git a/.github/workflows/webhooks.yml b/.github/workflows/webhooks.yml index 47d05e8a276a..dd6248e315c8 100644 --- a/.github/workflows/webhooks.yml +++ b/.github/workflows/webhooks.yml @@ -28,12 +28,12 @@ env: jobs: resolve-modules: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Resolve Modules id: set-matrix @@ -41,15 +41,15 @@ jobs: golangci-lint: needs: [ resolve-modules ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} @@ -57,25 +57,25 @@ jobs: run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev - name: Run Linter - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: version: v1.54.2 working-directory: ${{ matrix.workdir }} args: "--out-${NO_FUTURE}format colored-line-number" image-build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: module: [ admission ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v3 + uses: magnetikonline/action-golang-cache@v5 with: go-version: ${{ env.GO_VERSION }} @@ -107,12 +107,12 @@ jobs: name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: driver-opts: network=host - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: ${{ (github.event_name == 'push') || (inputs.push_mage == true) }} with: registry: ghcr.io diff --git a/README.md b/README.md index 5b35844dfe18..0b8903d64625 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -A Cloud Operating System designed for managing cloud-native applications +A Cloud Operating System for Managing Cloud Native Applications @@ -38,7 +38,7 @@ A Cloud Operating System designed for managing cloud-native applications
-[![discord](https://theme.zdassets.com/theme_assets/678183/cc59daa07820943e943c2fc283b9079d7003ff76.svg)](https://discord.gg/qzBmGGZGk7) +[![Discord](https://theme.zdassets.com/theme_assets/678183/cc59daa07820943e943c2fc283b9079d7003ff76.svg)](https://discord.gg/qzBmGGZGk7) @@ -54,47 +54,47 @@ Sealos['siːləs] is a cloud operating system distribution based on the Kubernet image -## Create any development environment in sealos with one click +## Create your Development Environment in Sealos with just one click -1. [Login in](https://cloud.sealos.run) , open sealos Devbox. +1. [Login](https://cloud.sealos.run) and open Sealos Devbox. image -2. Create a development env, any language, any framework. +2. Create a development environment, choosing from a range of languages and frameworks. image -3. Use vscode or cursor access to the Env. +3. Access your environment from a selection of IDEs, such as VSCode and Cursor. image image -## Create any database on sealos +## Create your Database on Sealos -1. [Login in](https://cloud.sealos.run) , open sealos database. +1. [Login](https://cloud.sealos.run), open Sealos database. image -2. Create Database. +2. Create your database. image -3. Access your database. +3. View your database and access details. image -## Deploy any docker image on sealos +## Deploy your Docker Image on Sealos -1. [Login in](https://cloud.sealos.run) , open sealos App launchpad. +1. [Login](https://cloud.sealos.run), open Sealos App Launchpad. - image + image -2. Deploy a docker image, ingress, deployment... +2. Deploy the Docker image using a Kubernetes Deployment and expose it with an Ingress. image -3. Access your service. +3. View your app details and access your service. image @@ -118,9 +118,9 @@ Sealos['siːləs] is a cloud operating system distribution based on the Kubernet ## 🏘️ Community & support -+ 🌐 Visit the [Sealos website](https://sealos.io/) for full documentation and useful links. -+ 💬 Join our [Discord server](https://discord.gg/qzBmGGZGk7) is to chat with Sealos developers and other Sealos users. This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences. -+ 🐦 Tweet at @sealosio on [Twitter](https://twitter.com/sealosio) and follow us. ++ 🌐 Visit the [Sealos Website](https://sealos.io/) for full documentation and useful links. ++ 💬 Join our [Discord Server](https://discord.gg/qzBmGGZGk7) is to chat with Sealos developers and other Sealos users. This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences. ++ 🐦 Tweet at @Sealos_io on [X/Twitter](https://x.com/Sealos_io) and follow us. + 🐞 Create [GitHub Issues](https://github.com/labring/sealos/issues/new/choose) for bug reports and feature requests. ## 🚧 Roadmap @@ -129,7 +129,7 @@ Sealos maintains a [public roadmap](https://github.com/orgs/labring/projects/4/v ## 👩‍💻 Contributing & Development -Have a look through [existing Issues](https://github.com/labring/sealos/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and [Pull Requests](https://github.com/labring/sealos/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc) that you could help with. If you'd like to request a feature or report a bug, please [create a GitHub Issue](https://github.com/labring/sealos/issues/new/choose) using one of the templates provided. +Have a look through [existing issues](https://github.com/labring/sealos/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and [Pull Requests](https://github.com/labring/sealos/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc) that you could help with. If you'd like to request a feature or report a bug, please [create a GitHub Issue](https://github.com/labring/sealos/issues/new/choose) using one of the templates provided. 📖 [See contribution guide →](./CONTRIBUTING.md) diff --git a/README_zh.md b/README_zh.md index 1336e1cf98bb..b10897ce2581 100644 --- a/README_zh.md +++ b/README_zh.md @@ -85,7 +85,7 @@ Sealos 是一款以 Kubernetes 为内核的**云操作系统发行版**。它以 - 💬 加入我们的 [Discord服务器](https://discord.gg/qzBmGGZGk7),与 Sealos 开发者和终端用户进行交流。这是了解 Sealos 和 Kubernetes 以及提问和分享经验的理想之地。 -- 🐦 在 [Twitter](https://twitter.com/sealosio) 上关注我们。 +- 🐦 在 [Twitter](https://twitter.com/Sailos_io) 上关注我们。 - 🐞 请将任何 Sealos 的 Bug、问题和需求提交到 [GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。 diff --git a/controllers/account/config/rbac/role.yaml b/controllers/account/config/rbac/role.yaml index ae3b9773c50b..26a337e80020 100644 --- a/controllers/account/config/rbac/role.yaml +++ b/controllers/account/config/rbac/role.yaml @@ -208,6 +208,18 @@ rules: - get - patch - update +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go index f7c21e42564a..a32b2911d239 100644 --- a/controllers/account/controllers/account_controller.go +++ b/controllers/account/controllers/account_controller.go @@ -59,6 +59,33 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +type CVMTaskRunner struct { + DBClient database.Interface + Logger logr.Logger + *AccountReconciler +} + +func (r *CVMTaskRunner) Start(ctx context.Context) error { + ticker := time.NewTicker(env.GetDurationEnvWithDefault("BILLING_CVM_INTERVAL", 10*time.Minute)) + defer func() { + ticker.Stop() + r.Logger.Info("stop billing cvm") + }() + for { + select { + case <-ticker.C: + r.Logger.Info("start billing cvm", "time", time.Now().Format(time.RFC3339)) + err := r.BillingCVM() + if err != nil { + r.Logger.Error(err, "fail to billing cvm") + } + r.Logger.Info("end billing cvm", "time", time.Now().Format(time.RFC3339)) + case <-ctx.Done(): + return nil + } + } +} + const ( ACCOUNTNAMESPACEENV = "ACCOUNT_NAMESPACE" DEFAULTACCOUNTNAMESPACE = "sealos-system" diff --git a/controllers/account/controllers/billing_controller.go b/controllers/account/controllers/billing_controller.go index 7d78299d9d82..71c2229d2bb9 100644 --- a/controllers/account/controllers/billing_controller.go +++ b/controllers/account/controllers/billing_controller.go @@ -22,27 +22,63 @@ import ( "strings" "time" + "k8s.io/client-go/rest" + + "k8s.io/client-go/kubernetes/scheme" + + "github.com/labring/sealos/controllers/pkg/utils/env" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + + ctrl "sigs.k8s.io/controller-runtime" + "github.com/labring/sealos/controllers/pkg/types" - v12 "github.com/labring/sealos/controllers/account/api/v1" "github.com/labring/sealos/controllers/pkg/resources" - "sigs.k8s.io/controller-runtime/pkg/controller" - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/labring/sealos/controllers/pkg/database" - v1 "github.com/labring/sealos/controllers/user/api/v1" "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) +type BillingTaskRunner struct { + *BillingReconciler +} + +func (r *BillingTaskRunner) Start(ctx context.Context) error { + if err := r.ExecuteBillingTask(); err != nil { + r.Logger.Error(err, "failed to execute billing task") + } + defer func() { + r.Logger.Info("stop billing reconcile", "time", time.Now().Format(time.RFC3339)) + }() + now := time.Now() + nextHour := now.Truncate(time.Hour).Add(time.Hour).Add(5 * time.Minute) + r.Logger.Info("next billing reconcile time", "time", nextHour.Format(time.RFC3339)) + time.Sleep(nextHour.Sub(now)) + + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + for { + if err := r.ExecuteBillingTask(); err != nil { + r.Logger.Error(err, "failed to execute billing task") + } + select { + case <-ticker.C: + if err := r.ExecuteBillingTask(); err != nil { + r.Logger.Error(err, "failed to execute billing task") + } + case <-ctx.Done(): + return nil + } + } +} + const ( UserNamespacePrefix = "ns-" ResourceQuotaPrefix = "quota-" @@ -55,83 +91,110 @@ type BillingReconciler struct { client.Client Scheme *runtime.Scheme logr.Logger - DBClient database.Account - AccountV2 database.AccountV2 - Properties *resources.PropertyTypeLS + DBClient database.Account + AccountV2 database.AccountV2 + Properties *resources.PropertyTypeLS + concurrentLimit int64 } -//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=resourcequotas,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch;delete - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the Billing object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile -func (r *BillingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Logger.V(1).Info("Reconcile Billing: ", "req.NamespacedName", req.NamespacedName) - ns := &corev1.Namespace{} - if err := r.Get(ctx, req.NamespacedName, ns); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - if ns.DeletionTimestamp != nil { - r.Logger.V(1).Info("namespace is deleting", "namespace", ns) - return ctrl.Result{}, nil - } - - owner := ns.Labels[v1.UserLabelOwnerKey] - nsList, err := getOwnNsList(r.Client, owner) +func (r *BillingReconciler) ExecuteBillingTask() error { + r.Logger.Info("start billing reconcile", "time", time.Now().Format(time.RFC3339)) + ownerListMap, err := r.getRecentUsedOwners() if err != nil { - r.Logger.Error(err, "get own namespace list failed") - return ctrl.Result{Requeue: true}, err + return fmt.Errorf("failed to get the owner list of the recently used resource: %w", err) + } + err = r.reconcileOwnerListBatch(ownerListMap, env.GetIntEnvWithDefault("BILLING_RECONCILE_BATCH_COUNT", 200), time.Now(), r.reconcileOwnerList) + if err != nil { + return fmt.Errorf("failed to reconcile owner list batch: %w", err) + } + r.Logger.Info("finish billing reconcile", "time", time.Now().Format(time.RFC3339)) + return nil +} + +func (r *BillingReconciler) reconcileOwnerList(ownerListMap map[string][]string, now time.Time) error { + endHourTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local).UTC() + startHourTime := endHourTime.Add(-1 * time.Hour) + var ownerList, failedList []string + for owner := range ownerListMap { + ownerList = append(ownerList, owner) + } + updateOwnerList, err := r.DBClient.GetOwnersRecentUpdates(ownerList, endHourTime) + if err != nil { + return fmt.Errorf("get owners without recent updates failed: %w", err) } - r.Logger.V(1).Info("own namespace list", "own", owner, "nsList", nsList) - now := time.Now() - currentHourTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local).UTC() - queryTime := currentHourTime.Add(-1 * time.Hour) - // TODO r.处理Unsettle状态的账单 + // remove the owner that does not need to be updated + for _, owner := range updateOwnerList { + delete(ownerListMap, owner) + } + r.Logger.Info("get owners recent updates", "already update owner count", len(updateOwnerList), "remaining owner count", len(ownerListMap)) - if exist, lastUpdateTime, _ := r.DBClient.GetBillingLastUpdateTime(owner, v12.Consumption); exist { - if lastUpdateTime.Equal(currentHourTime) || lastUpdateTime.After(currentHourTime) { - return ctrl.Result{Requeue: true, RequeueAfter: time.Until(currentHourTime.Add(1*time.Hour + 10*time.Minute))}, nil + ownerBillings, err := r.DBClient.GenerateBillingData(startHourTime, endHourTime, r.Properties, ownerListMap) + if err != nil { + return fmt.Errorf("generate billing data failed: %w", err) + } + r.Logger.Info("generate billing data", "count", len(ownerBillings)) + for owner, billings := range ownerBillings { + amount := int64(0) + orderIDs := make([]string, 0, len(billings)) + for _, billing := range billings { + amount += billing.Amount + orderIDs = append(orderIDs, billing.OrderID) } - // 24小时内的数据,从上次更新时间开始计算,否则从当前时间起算 - if lastUpdateTime.After(currentHourTime.Add(-24 * time.Hour)) { - queryTime = lastUpdateTime + if err = r.DBClient.SaveBillings(billings...); err != nil { + r.Logger.Error(err, "save billings failed", "owner", owner, "amount", amount) + failedList = append(failedList, owner) + continue } + if err := r.rechargeBalance(owner, amount); err != nil { + r.Logger.Error(err, "recharge balance failed", "owner", owner, "amount", amount) + failedList = append(failedList, owner) + if err := r.DBClient.UpdateBillingStatus(orderIDs, resources.Unsettled); err != nil { + r.Logger.Error(err, "update billing unsettled status failed", "orderIDs", orderIDs) + } + } + } + if len(failedList) > 0 { + r.Logger.Error(fmt.Errorf("failed to reconcile owner list: %v", failedList), "failed to reconcile owner list") } + return nil +} - orderList := []string{} - consumAmount := int64(0) - // 计算上次billing到当前的时间之间的整点,左开右闭 - for t := queryTime.Truncate(time.Hour).Add(time.Hour); t.Before(currentHourTime) || t.Equal(currentHourTime); t = t.Add(time.Hour) { - ids, amount, err := r.DBClient.GenerateBillingData(t.Add(-1*time.Hour), t, r.Properties, nsList, getUsername(owner)) - if err != nil { - return ctrl.Result{}, fmt.Errorf("generate billing data failed: %w", err) +// reconcileOwnerListBatch process ownerlistmap in batch mode +func (r *BillingReconciler) reconcileOwnerListBatch( + ownerListMap map[string][]string, // The owner -> namespaces mapping needs to be handled + batchSize int, // number of owners processed per batch + now time.Time, // current time + reconcileFunc func(map[string][]string, time.Time) error, // processing function +) error { + if batchSize <= 0 { + return fmt.Errorf("batch size must be greater than zero") + } + + owners := make([]string, 0, len(ownerListMap)) // store all owners + for owner := range ownerListMap { + owners = append(owners, owner) + } + + total := len(owners) + for i := 0; i < total; i += batchSize { + end := i + batchSize + if end > total { + end = total } - orderList = append(orderList, ids...) - consumAmount += amount - } - if consumAmount > 0 { - if err := r.rechargeBalance(owner, consumAmount); err != nil { - for i := range orderList { - if err := r.DBClient.UpdateBillingStatus(orderList[i], resources.Unsettled); err != nil { - r.Logger.Error(err, "update billing status failed", "id", orderList[i]) - } - } - return ctrl.Result{}, fmt.Errorf("recharge balance failed: %w", err) + + batchOwners := owners[i:end] // the owner list of the current batch + batchOwnerMap := make(map[string][]string, len(batchOwners)) + for _, owner := range batchOwners { + batchOwnerMap[owner] = ownerListMap[owner] // example retrieve a namespace + } + // call processing logic + if err := reconcileFunc(batchOwnerMap, now); err != nil { + return fmt.Errorf("failed to reconcile batch from %d to %d: %w", i, end, err) } - r.Logger.V(1).Info("success recharge balance", "owner", owner, "amount", consumAmount) + r.Logger.Info("reconcile batch", "from", i, "to", end) } - return ctrl.Result{Requeue: true, RequeueAfter: time.Until(currentHourTime.Add(1*time.Hour + 10*time.Minute))}, nil + return nil } func (r *BillingReconciler) rechargeBalance(owner string, amount int64) (err error) { @@ -144,48 +207,90 @@ func (r *BillingReconciler) rechargeBalance(owner string, amount int64) (err err return nil } -func getOwnNsList(clt client.Client, user string) ([]string, error) { - nsList := &corev1.NamespaceList{} - if err := clt.List(context.Background(), nsList, client.MatchingLabels{v1.UserLabelOwnerKey: user}); err != nil { - return nil, fmt.Errorf("list namespace failed: %w", err) +func (r *BillingReconciler) getRecentUsedOwners() (map[string][]string, error) { + now := time.Now() + endHourTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local).UTC() + startHourTime := endHourTime.Add(-1 * time.Hour) + namespaceList, err := r.DBClient.GetTimeUsedNamespaceList(startHourTime, endHourTime) + if err != nil { + return nil, fmt.Errorf("get recent owners failed: %w", err) + } + nsToOwnerMap, err := GetAllUser() + if err != nil { + return nil, fmt.Errorf("get all user failed: %w", err) } - nsListStr := make([]string, len(nsList.Items)) - for i := range nsList.Items { - nsListStr[i] = nsList.Items[i].Name + r.Logger.Info("get owner and namespace", "owner count", len(nsToOwnerMap), "namespace count", len(namespaceList)) + usedOwnerList := make(map[string][]string) + for _, ns := range namespaceList { + if owner, ok := nsToOwnerMap[ns]; ok { + if _, ok := usedOwnerList[owner]; !ok { + usedOwnerList[owner] = []string{} + } + usedOwnerList[owner] = append(usedOwnerList[owner], ns) + } } - return nsListStr, nil + r.Logger.Info("get all user", "count", len(usedOwnerList)) + return usedOwnerList, nil } -func (r *BillingReconciler) initDB() error { - return r.DBClient.CreateBillingIfNotExist() +func getUsername(namespace string) string { + return strings.TrimPrefix(namespace, UserNamespacePrefix) } -// SetupWithManager sets up the controller with the Manager. -func (r *BillingReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { +func (r *BillingReconciler) Init() error { r.Logger = ctrl.Log.WithName("controller").WithName("Billing") - if err := r.initDB(); err != nil { - r.Logger.Error(err, "init db failed") - } - return ctrl.NewControllerManagedBy(mgr). - For(&corev1.Namespace{}, builder.WithPredicates(predicate.Funcs{ - CreateFunc: func(createEvent event.CreateEvent) bool { - own, ok := createEvent.Object.GetLabels()[v1.UserLabelOwnerKey] - return ok && getUsername(createEvent.Object.GetName()) == own - }, - UpdateFunc: func(_ event.UpdateEvent) bool { - return false - }, - DeleteFunc: func(_ event.DeleteEvent) bool { - return false - }, - GenericFunc: func(_ event.GenericEvent) bool { - return false - }, - })). - WithOptions(rateOpts). - Complete(r) + if err := r.DBClient.CreateBillingIfNotExist(); err != nil { + return fmt.Errorf("create billing collection failed: %w", err) + } + r.concurrentLimit = env.GetInt64EnvWithDefault("BILLING_CONCURRENT_LIMIT", 100) + return nil } -func getUsername(namespace string) string { - return strings.TrimPrefix(namespace, UserNamespacePrefix) +// map[namespace]owner +func GetAllUser() (map[string]string, error) { + err := userv1.AddToScheme(scheme.Scheme) + if err != nil { + return nil, fmt.Errorf("unable to add scheme: %v", err) + } + config, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("unable to build config: %v", err) + } + //TODO from cluster config + //config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) + //if err != nil { + // return nil, fmt.Errorf("unable to build config: %v", err) + //} + k8sClt, err := client.New(config, client.Options{Scheme: scheme.Scheme}) + if err != nil { + return nil, fmt.Errorf("unable to create client: %v", err) + } + nsToOwnerMap := make(map[string]string) + + listOpts := &client.ListOptions{ + Limit: 5000, + } + for { + userMetaList := &metav1.PartialObjectMetadataList{} + userMetaList.SetGroupVersionKind(userv1.GroupVersion.WithKind("UserList")) + + if err := k8sClt.List(context.Background(), userMetaList, listOpts); err != nil { + return nil, fmt.Errorf("failed to list instances: %v", err) + } + + for _, user := range userMetaList.Items { + owner := user.Annotations[userv1.UserLabelOwnerKey] + if owner == "" { + continue + } + nsToOwnerMap["ns-"+user.Name] = owner + } + + token := userMetaList.GetContinue() + if token == "" { + break + } + listOpts.Continue = token + } + return nsToOwnerMap, nil } diff --git a/controllers/account/controllers/debt_controller.go b/controllers/account/controllers/debt_controller.go index 8a3de2cb7fc1..b620b998e622 100644 --- a/controllers/account/controllers/debt_controller.go +++ b/controllers/account/controllers/debt_controller.go @@ -246,6 +246,18 @@ func (r *DebtReconciler) reconcile(ctx context.Context, userCr, userID string) e return nil } +func getOwnNsList(clt client.Client, user string) ([]string, error) { + nsList := &corev1.NamespaceList{} + if err := clt.List(context.Background(), nsList, client.MatchingLabels{userv1.UserLabelOwnerKey: user}); err != nil { + return nil, fmt.Errorf("list namespace failed: %w", err) + } + nsListStr := make([]string, len(nsList.Items)) + for i := range nsList.Items { + nsListStr[i] = nsList.Items[i].Name + } + return nsListStr, nil +} + var ErrAccountNotExist = errors.New("account not exist") /* diff --git a/controllers/account/controllers/namespace_controller.go b/controllers/account/controllers/namespace_controller.go index 960e310a0357..f825e4e8b191 100644 --- a/controllers/account/controllers/namespace_controller.go +++ b/controllers/account/controllers/namespace_controller.go @@ -23,6 +23,8 @@ import ( "strings" "time" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/minio/madmin-go/v3" @@ -42,6 +44,7 @@ import ( kbv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/go-logr/logr" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -73,6 +76,7 @@ const ( //+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=namespaces/status,verbs=get;update;patch //+kubebuilder:rbac:groups=core,resources=namespaces/finalizers,verbs=update +//+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusters/status,verbs=get;update;patch @@ -132,16 +136,16 @@ func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } func (r *NamespaceReconciler) SuspendUserResource(ctx context.Context, namespace string) error { + // suspend kb cluster // limit0 resource quota // suspend pod: deploy pod && clone unmanaged pod - // delete infra cr + // suspend cronjob pipelines := []func(context.Context, string) error{ r.suspendKBCluster, r.suspendOrphanPod, r.limitResourceQuotaCreate, r.deleteControlledPod, - //TODO how to suspend infra cr or delete infra cr - //r.suspendInfraResources, + r.suspendCronJob, r.suspendObjectStorage, } for _, fn := range pipelines { @@ -485,3 +489,20 @@ func (AnnotationChangedPredicate) Create(e event.CreateEvent) bool { _, ok := e.Object.GetAnnotations()[v1.DebtNamespaceAnnoStatusKey] return ok } + +func (r *NamespaceReconciler) suspendCronJob(ctx context.Context, namespace string) error { + cronJobList := batchv1.CronJobList{} + if err := r.Client.List(ctx, &cronJobList, client.InNamespace(namespace)); err != nil { + return err + } + for _, cronJob := range cronJobList.Items { + if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend { + continue + } + cronJob.Spec.Suspend = ptr.To(true) + if err := r.Client.Update(ctx, &cronJob); err != nil { + return fmt.Errorf("failed to suspend cronjob %s: %w", cronJob.Name, err) + } + } + return nil +} diff --git a/controllers/account/controllers/payment_controller.go b/controllers/account/controllers/payment_controller.go index 1a016bc44a7d..1ae5f1c8dc81 100644 --- a/controllers/account/controllers/payment_controller.go +++ b/controllers/account/controllers/payment_controller.go @@ -23,6 +23,8 @@ import ( "sync" "time" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/google/uuid" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -319,7 +321,7 @@ func (r *PaymentReconciler) reconcileNewPayment(payment *accountv1.Payment) erro if err != nil { return fmt.Errorf("get payment Interface failed: %w", err) } - tradeNO, codeURL, err := payHandler.CreatePayment(payment.Spec.Amount/10000, payment.Spec.UserID, "sealos cloud pay [domain="+r.domain+"]") + tradeNO, codeURL, err := payHandler.CreatePayment(payment.Spec.Amount/10000, payment.Spec.UserID, fmt.Sprintf(env.GetEnvWithDefault("PAY_DESCRIBE_FORMAT", `sealos cloud pay [domain="%s"]`), r.domain)) if err != nil { return fmt.Errorf("get tradeNO and codeURL failed: %w", err) } diff --git a/controllers/account/deploy/manifests/deploy.yaml b/controllers/account/deploy/manifests/deploy.yaml index 6ae9808efe5c..ea4b379d9072 100644 --- a/controllers/account/deploy/manifests/deploy.yaml +++ b/controllers/account/deploy/manifests/deploy.yaml @@ -421,6 +421,18 @@ rules: - get - patch - update +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: diff --git a/controllers/account/main.go b/controllers/account/main.go index 9f875848ac97..5ee6415ce9dc 100644 --- a/controllers/account/main.go +++ b/controllers/account/main.go @@ -29,7 +29,6 @@ import ( notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" "github.com/labring/sealos/controllers/pkg/resources" "github.com/labring/sealos/controllers/pkg/types" - "github.com/labring/sealos/controllers/pkg/utils/env" rate "github.com/labring/sealos/controllers/pkg/utils/rate" userv1 "github.com/labring/sealos/controllers/user/api/v1" @@ -218,14 +217,23 @@ func main() { setupLog.Error(err, "unable to get property type") os.Exit(1) } - if err = (&controllers.BillingReconciler{ + billingReconciler := controllers.BillingReconciler{ DBClient: dbClient, Properties: resources.DefaultPropertyTypeLS, Client: mgr.GetClient(), Scheme: mgr.GetScheme(), AccountV2: v2Account, - }).SetupWithManager(mgr, rateOpts); err != nil { - setupManagerError(err, "Billing") + } + if err = billingReconciler.Init(); err != nil { + setupLog.Error(err, "unable to init billing reconciler") + os.Exit(1) + } + billingTaskRunner := &controllers.BillingTaskRunner{ + BillingReconciler: &billingReconciler, + } + if err := mgr.Add(billingTaskRunner); err != nil { + setupLog.Error(err, "unable to add billing task runner") + os.Exit(1) } if err = (&controllers.PodReconciler{ @@ -261,23 +269,32 @@ func main() { os.Exit(1) } - go func() { - if cvmDBClient == nil { - setupLog.Info("CVM DB client is nil, skip billing cvm") - return + if cvmDBClient != nil { + cvmTaskRunner := &controllers.CVMTaskRunner{ + DBClient: cvmDBClient, + Logger: ctrl.Log.WithName("CVMTaskRunner"), + AccountReconciler: accountReconciler, } - ticker := time.NewTicker(env.GetDurationEnvWithDefault("BILLING_CVM_INTERVAL", 10*time.Minute)) - defer ticker.Stop() - for { - setupLog.Info("start billing cvm", "time", time.Now().Format(time.RFC3339)) - err := accountReconciler.BillingCVM() - if err != nil { - setupLog.Error(err, "fail to billing cvm") - } - setupLog.Info("end billing cvm", "time", time.Now().Format(time.RFC3339)) - <-ticker.C + if err := mgr.Add(cvmTaskRunner); err != nil { + setupLog.Error(err, "unable to add cvm task runner") + os.Exit(1) } - }() + } + //go func() { + // now := time.Now() + // nextHour := now.Truncate(time.Hour).Add(time.Hour) + // time.Sleep(nextHour.Sub(now)) + // + // ticker := time.NewTicker(time.Hour) + // defer ticker.Stop() + // for { + // setupLog.Info("start billing reconcile", "time", time.Now().Format(time.RFC3339)) + // if err := billingReconciler.ExecuteBillingTask(); err != nil { + // setupLog.Error(err, "failed to execute billing task") + // } + // <-ticker.C + // } + //}() setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { diff --git a/controllers/devbox/api/v1alpha1/devbox_types.go b/controllers/devbox/api/v1alpha1/devbox_types.go index 5c82533e142e..165cbea66601 100644 --- a/controllers/devbox/api/v1alpha1/devbox_types.go +++ b/controllers/devbox/api/v1alpha1/devbox_types.go @@ -18,17 +18,10 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type ResourceName string - const ( - // ResourceCPU CPU, in cores. (500m = .5 cores) - ResourceCPU ResourceName = "cpu" - // ResourceMemory Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) - ResourceMemory ResourceName = "memory" // FinalizerName is the finalizer for Devbox FinalizerName = "devbox.sealos.io/finalizer" DevBoxPartOf = "devbox" @@ -52,8 +45,6 @@ const ( NetworkTypeTailnet NetworkType = "Tailnet" ) -type ResourceList map[ResourceName]resource.Quantity - type RuntimeRef struct { // +kubebuilder:validation:Required Name string `json:"name"` @@ -69,46 +60,75 @@ type NetworkSpec struct { ExtraPorts []corev1.ContainerPort `json:"extraPorts"` } +type Config struct { + // +kubebuilder:validation:Optional + // +kubebuilder:default=devbox + User string `json:"user"` + + // +kubebuilder:validation:Optional + Labels map[string]string `json:"labels,omitempty"` + // +kubebuilder:validation:Optional + Annotations map[string]string `json:"annotations,omitempty"` + + // +kubebuilder:validation:Optional + Command []string `json:"command,omitempty"` + // kubebuilder:validation:Optional + Args []string `json:"args,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:default=/home/devbox/project + WorkingDir string `json:"workingDir,omitempty"` + // +kubebuilder:validation:Optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default={/bin/bash,-c} + ReleaseCommand []string `json:"releaseCommand,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:default={/home/devbox/project/entrypoint.sh} + ReleaseArgs []string `json:"releaseArgs,omitempty"` + + // TODO: in v1alpha2 api we need fix the port and app port into one field and create a new type for it. + // +kubebuilder:validation:Optional + // +kubebuilder:default={{name:"devbox-ssh-port",containerPort:22,protocol:TCP}} + Ports []corev1.ContainerPort `json:"ports,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:default={{name:"devbox-app-port",port:8080,protocol:TCP}} + AppPorts []corev1.ServicePort `json:"appPorts,omitempty"` + + // +kubebuilder:validation:Optional + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` + // +kubebuilder:validation:Optional + Volumes []corev1.Volume `json:"volumes,omitempty"` +} + // DevboxSpec defines the desired state of Devbox type DevboxSpec struct { // +kubebuilder:validation:Required // +kubebuilder:validation:Enum=Running;Stopped State DevboxState `json:"state"` // +kubebuilder:validation:Required - Resource ResourceList `json:"resource"` + Resource corev1.ResourceList `json:"resource"` // +kubebuilder:validation:Optional // +kubebuilder:default=false Squash bool `json:"squash"` // +kubebuilder:validation:Required - RuntimeRef RuntimeRef `json:"runtimeRef"` - - // +kubebuilder:validation:Required - NetworkSpec NetworkSpec `json:"network,omitempty"` + Image string `json:"image"` - // todo add rewrite labels and annotations... - // +kubebuilder:validation:Optional - ExtraLabels map[string]string `json:"extraLabels,omitempty"` // +kubebuilder:validation:Optional - ExtraAnnotations map[string]string `json:"extraAnnotations,omitempty"` + TemplateID string `json:"templateID"` - // +kubebuilder:validation:Optional - Command []string `json:"command,omitempty"` - // +kubebuilder:validation:Optional - Args []string `json:"args,omitempty"` - // +kubebuilder:validation:Optional - WorkingDir string `json:"workingDir,omitempty"` - // todo add rewrite env... - // +kubebuilder:validation:Optional - ExtraEnvs []corev1.EnvVar `json:"extraEnvs"` + // +kubebuilder:validation:Required + Config Config `json:"config"` + + // +kubebuilder:validation:Required + NetworkSpec NetworkSpec `json:"network,omitempty"` - // todo add rewrite volumes and volume mounts.. // +kubebuilder:validation:Optional - ExtraVolumes []corev1.Volume `json:"extraVolumes,omitempty"` + RuntimeClassName string `json:"runtimeClassName,omitempty"` // +kubebuilder:validation:Optional - ExtraVolumeMounts []corev1.VolumeMount `json:"extraVolumeMounts,omitempty"` - + NodeSelector map[string]string `json:"nodeSelector,omitempty"` // +kubebuilder:validation:Optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` // +kubebuilder:validation:Optional @@ -189,8 +209,6 @@ type DevboxStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="State",type="string",JSONPath=".spec.state" -// +kubebuilder:printcolumn:name="RuntimeRef",type="string",JSONPath=".spec.runtimeRef.name" -// +kubebuilder:printcolumn:name="PodPhase",type="string",JSONPath=".status.podPhase" // +kubebuilder:printcolumn:name="NetworkType",type="string",JSONPath=".status.network.type" // +kubebuilder:printcolumn:name="NodePort",type="integer",JSONPath=".status.network.nodePort" // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" diff --git a/controllers/devbox/api/v1alpha1/runtime_types.go b/controllers/devbox/api/v1alpha1/runtime_types.go deleted file mode 100644 index 52791dbf79b2..000000000000 --- a/controllers/devbox/api/v1alpha1/runtime_types.go +++ /dev/null @@ -1,140 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type Config struct { - // +kubebuilder:validation:Optional - // +kubebuilder:default=sealos - User string `json:"user"` - - // +kubebuilder:validation:Required - Image string `json:"image"` - - // +kubebuilder:validation:Optional - Labels map[string]string `json:"labels,omitempty"` - // +kubebuilder:validation:Optional - Annotations map[string]string `json:"annotations,omitempty"` - - // +kubebuilder:validation:Optional - Command []string `json:"command,omitempty"` - // kubebuilder:validation:Optional - Args []string `json:"args,omitempty"` - // +kubebuilder:validation:Optional - // +kubebuilder:default=/home/sealos/project - WorkingDir string `json:"workingDir,omitempty"` - // +kubebuilder:validation:Optional - Env []corev1.EnvVar `json:"env,omitempty"` - - // +kubebuilder:validation:Optional - // +kubebuilder:default={/bin/bash,-c} - ReleaseCommand []string `json:"releaseCommand,omitempty"` - // +kubebuilder:validation:Optional - // +kubebuilder:default={/home/sealos/project/entrypoint.sh} - ReleaseArgs []string `json:"releaseArgs,omitempty"` - - // TODO: in v1alpha2 api we need fix the port and app port into one field and create a new type for it. - // +kubebuilder:validation:Optional - // +kubebuilder:default={{name:"devbox-ssh-port",containerPort:22,protocol:TCP}} - Ports []corev1.ContainerPort `json:"ports,omitempty"` - // +kubebuilder:validation:Optional - // +kubebuilder:default={{name:"devbox-app-port",port:8080,protocol:TCP}} - AppPorts []corev1.ServicePort `json:"appPorts,omitempty"` - - // +kubebuilder:validation:Optional - VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` - // +kubebuilder:validation:Optional - Volumes []corev1.Volume `json:"volumes,omitempty"` -} - -type Component struct { - // +kubebuilder:validation:Required - Name string `json:"name"` - // +kubebuilder:validation:Required - Version string `json:"version"` -} - -type RuntimeState string - -const ( - RuntimeStateActive RuntimeState = "active" - RuntimeStateDeprecated RuntimeState = "deprecated" -) - -// RuntimeSpec defines the desired state of Runtime -type RuntimeSpec struct { - // +kubebuilder:validation:Required - ClassRef string `json:"classRef"` - // +kubebuilder:validation:Required - Version string `json:"version"` - - // +kubebuilder:validation:Optional - Components []Component `json:"components,omitempty"` - // +kubebuilder:validation:Optional - Category []string `json:"category,omitempty"` - // +kube:validation:Optional - Description string `json:"description,omitempty"` - - // +kubebuilder:validation:Required - Config Config `json:"config"` - - // +kubebuilder:validation:Optional - RuntimeVersion string `json:"runtimeVersion,omitempty"` - // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=active;deprecated - // +kubebuilder:default=active - State RuntimeState `json:"state,omitempty"` -} - -// RuntimeStatus defines the observed state of Runtime -type RuntimeStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Class",type=string,JSONPath=`.spec.classRef` -// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=`.spec.version` -// +kubebuilder:printcolumn:name="RuntimeVersion",type=string,JSONPath=`.spec.runtimeVersion` -// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.spec.state` - -// Runtime is the Schema for the runtimes API -type Runtime struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec RuntimeSpec `json:"spec,omitempty"` - Status RuntimeStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// RuntimeList contains a list of Runtime -type RuntimeList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Runtime `json:"items"` -} - -func init() { - SchemeBuilder.Register(&Runtime{}, &RuntimeList{}) -} diff --git a/controllers/devbox/api/v1alpha1/runtimeclass_types.go b/controllers/devbox/api/v1alpha1/runtimeclass_types.go deleted file mode 100644 index 8d8599e5ebda..000000000000 --- a/controllers/devbox/api/v1alpha1/runtimeclass_types.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type RuntimeClassKind string - -const ( - RuntimeClassOSKind RuntimeClassKind = "OS" - RuntimeClassLanguageKind RuntimeClassKind = "Language" - RuntimeClassFrameworkKind RuntimeClassKind = "Framework" -) - -// RuntimeClassSpec defines the desired state of RuntimeClass -type RuntimeClassSpec struct { - // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=OS;Language;Framework - Kind RuntimeClassKind `json:"kind"` - // +kubebuilder:validation:Required - Title string `json:"title"` - // +kubebuilder:validation:Optional - Description string `json:"description"` -} - -// RuntimeClassStatus defines the observed state of RuntimeClass -type RuntimeClassStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status - -// RuntimeClass is the Schema for the runtimeclasses API -type RuntimeClass struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec RuntimeClassSpec `json:"spec,omitempty"` - Status RuntimeClassStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// RuntimeClassList contains a list of RuntimeClass -type RuntimeClassList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []RuntimeClass `json:"items"` -} - -func init() { - SchemeBuilder.Register(&RuntimeClass{}, &RuntimeClassList{}) -} diff --git a/controllers/devbox/api/v1alpha1/zz_generated.deepcopy.go b/controllers/devbox/api/v1alpha1/zz_generated.deepcopy.go index cbf872db25e7..d1590df7e111 100644 --- a/controllers/devbox/api/v1alpha1/zz_generated.deepcopy.go +++ b/controllers/devbox/api/v1alpha1/zz_generated.deepcopy.go @@ -41,21 +41,6 @@ func (in *CommitHistory) DeepCopy() *CommitHistory { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Component) DeepCopyInto(out *Component) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Component. -func (in *Component) DeepCopy() *Component { - if in == nil { - return nil - } - out := new(Component) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Config) DeepCopyInto(out *Config) { *out = *in @@ -291,58 +276,20 @@ func (in *DevboxSpec) DeepCopyInto(out *DevboxSpec) { *out = *in if in.Resource != nil { in, out := &in.Resource, &out.Resource - *out = make(ResourceList, len(*in)) + *out = make(v1.ResourceList, len(*in)) for key, val := range *in { (*out)[key] = val.DeepCopy() } } - out.RuntimeRef = in.RuntimeRef + in.Config.DeepCopyInto(&out.Config) in.NetworkSpec.DeepCopyInto(&out.NetworkSpec) - if in.ExtraLabels != nil { - in, out := &in.ExtraLabels, &out.ExtraLabels - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.ExtraAnnotations != nil { - in, out := &in.ExtraAnnotations, &out.ExtraAnnotations + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } - if in.Command != nil { - in, out := &in.Command, &out.Command - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Args != nil { - in, out := &in.Args, &out.Args - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.ExtraEnvs != nil { - in, out := &in.ExtraEnvs, &out.ExtraEnvs - *out = make([]v1.EnvVar, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.ExtraVolumes != nil { - in, out := &in.ExtraVolumes, &out.ExtraVolumes - *out = make([]v1.Volume, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.ExtraVolumeMounts != nil { - in, out := &in.ExtraVolumeMounts, &out.ExtraVolumeMounts - *out = make([]v1.VolumeMount, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations *out = make([]v1.Toleration, len(*in)) @@ -520,175 +467,6 @@ func (in *OperationRequestStatus) DeepCopy() *OperationRequestStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in ResourceList) DeepCopyInto(out *ResourceList) { - { - in := &in - *out = make(ResourceList, len(*in)) - for key, val := range *in { - (*out)[key] = val.DeepCopy() - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceList. -func (in ResourceList) DeepCopy() ResourceList { - if in == nil { - return nil - } - out := new(ResourceList) - in.DeepCopyInto(out) - return *out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Runtime) DeepCopyInto(out *Runtime) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Runtime. -func (in *Runtime) DeepCopy() *Runtime { - if in == nil { - return nil - } - out := new(Runtime) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Runtime) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeClass) DeepCopyInto(out *RuntimeClass) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeClass. -func (in *RuntimeClass) DeepCopy() *RuntimeClass { - if in == nil { - return nil - } - out := new(RuntimeClass) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RuntimeClass) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeClassList) DeepCopyInto(out *RuntimeClassList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]RuntimeClass, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeClassList. -func (in *RuntimeClassList) DeepCopy() *RuntimeClassList { - if in == nil { - return nil - } - out := new(RuntimeClassList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RuntimeClassList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeClassSpec) DeepCopyInto(out *RuntimeClassSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeClassSpec. -func (in *RuntimeClassSpec) DeepCopy() *RuntimeClassSpec { - if in == nil { - return nil - } - out := new(RuntimeClassSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeClassStatus) DeepCopyInto(out *RuntimeClassStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeClassStatus. -func (in *RuntimeClassStatus) DeepCopy() *RuntimeClassStatus { - if in == nil { - return nil - } - out := new(RuntimeClassStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeList) DeepCopyInto(out *RuntimeList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Runtime, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeList. -func (in *RuntimeList) DeepCopy() *RuntimeList { - if in == nil { - return nil - } - out := new(RuntimeList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RuntimeList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RuntimeRef) DeepCopyInto(out *RuntimeRef) { *out = *in @@ -703,44 +481,3 @@ func (in *RuntimeRef) DeepCopy() *RuntimeRef { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeSpec) DeepCopyInto(out *RuntimeSpec) { - *out = *in - if in.Components != nil { - in, out := &in.Components, &out.Components - *out = make([]Component, len(*in)) - copy(*out, *in) - } - if in.Category != nil { - in, out := &in.Category, &out.Category - *out = make([]string, len(*in)) - copy(*out, *in) - } - in.Config.DeepCopyInto(&out.Config) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeSpec. -func (in *RuntimeSpec) DeepCopy() *RuntimeSpec { - if in == nil { - return nil - } - out := new(RuntimeSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeStatus) DeepCopyInto(out *RuntimeStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeStatus. -func (in *RuntimeStatus) DeepCopy() *RuntimeStatus { - if in == nil { - return nil - } - out := new(RuntimeStatus) - in.DeepCopyInto(out) - return out -} diff --git a/controllers/devbox/cmd/main.go b/controllers/devbox/cmd/main.go index 62416e9441da..ac3bd5fbf25c 100644 --- a/controllers/devbox/cmd/main.go +++ b/controllers/devbox/cmd/main.go @@ -27,6 +27,7 @@ import ( "k8s.io/client-go/rest" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -42,7 +43,9 @@ import ( devboxv1alpha1 "github.com/labring/sealos/controllers/devbox/api/v1alpha1" "github.com/labring/sealos/controllers/devbox/internal/controller" + "github.com/labring/sealos/controllers/devbox/internal/controller/utils/matcher" "github.com/labring/sealos/controllers/devbox/internal/controller/utils/registry" + utilresource "github.com/labring/sealos/controllers/devbox/internal/controller/utils/resource" // +kubebuilder:scaffold:imports ) @@ -65,19 +68,26 @@ func main() { var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) + // debug flag + var debugMode bool + // registry flag var registryAddr string var registryUser string var registryPassword string - var authAddr string + // resource flag var requestCPURate float64 var requestMemoryRate float64 var requestEphemeralStorage string var limitEphemeralStorage string - var debugMode bool - flag.StringVar(®istryAddr, "registry-addr", "sealos.hub:5000", "The address of the registry") - flag.StringVar(®istryUser, "registry-user", "admin", "The user of the registry") - flag.StringVar(®istryPassword, "registry-password", "passw0rd", "The password of the registry") - flag.StringVar(&authAddr, "auth-addr", "sealos.hub:5000", "The address of the auth") + var maximumLimitEphemeralStorage string + // pod matcher flag + var enablePodResourceMatcher bool + var enablePodEnvMatcher bool + var enablePodPortMatcher bool + var enablePodEphemeralStorageMatcher bool + // config qps and burst + var configQPS int + var configBurst int flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -88,11 +98,26 @@ func main() { "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") + // debug flag flag.BoolVar(&debugMode, "debug", false, "If set, debug mode will be enabled") + // registry flag + flag.StringVar(®istryAddr, "registry-addr", "sealos.hub:5000", "The address of the registry") + flag.StringVar(®istryUser, "registry-user", "admin", "The user of the registry") + flag.StringVar(®istryPassword, "registry-password", "passw0rd", "The password of the registry") + // resource flag flag.Float64Var(&requestCPURate, "request-cpu-rate", 10, "The request rate of cpu limit in devbox.") flag.Float64Var(&requestMemoryRate, "request-memory-rate", 10, "The request rate of memory limit in devbox.") - flag.StringVar(&requestEphemeralStorage, "request-ephemeral-storage", "500Mi", "The request value of ephemeral storage in devbox.") - flag.StringVar(&limitEphemeralStorage, "limit-ephemeral-storage", "10Gi", "The limit value of ephemeral storage in devbox.") + flag.StringVar(&requestEphemeralStorage, "request-ephemeral-storage", "500Mi", "The default request value of ephemeral storage in devbox.") + flag.StringVar(&limitEphemeralStorage, "limit-ephemeral-storage", "10Gi", "The default limit value of ephemeral storage in devbox.") + flag.StringVar(&maximumLimitEphemeralStorage, "maximum-limit-ephemeral-storage", "50Gi", "The maximum limit value of ephemeral storage in devbox.") + // pod matcher flag, pod resource matcher, env matcher, port matcher will be enabled by default, ephemeral storage matcher will be disabled by default + flag.BoolVar(&enablePodResourceMatcher, "enable-pod-resource-matcher", true, "If set, pod resource matcher will be enabled") + flag.BoolVar(&enablePodEnvMatcher, "enable-pod-env-matcher", true, "If set, pod env matcher will be enabled") + flag.BoolVar(&enablePodPortMatcher, "enable-pod-port-matcher", true, "If set, pod port matcher will be enabled") + flag.BoolVar(&enablePodEphemeralStorageMatcher, "enable-pod-ephemeral-storage-matcher", false, "If set, pod ephemeral storage matcher will be enabled") + // config qps and burst + flag.IntVar(&configQPS, "config-qps", 50, "The qps of the config") + flag.IntVar(&configBurst, "config-burst", 100, "The burst of the config") opts := zap.Options{ Development: true, } @@ -149,7 +174,12 @@ func main() { "app.kubernetes.io/part-of": "devbox", }) - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + config := ctrl.GetConfigOrDie() + // set qps and burst to config qps and burst for kube-config + config.QPS = float32(configQPS) + config.Burst = configBurst + + mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, @@ -182,16 +212,36 @@ func main() { os.Exit(1) } + podMatchers := []matcher.PodMatcher{} + if enablePodResourceMatcher { + podMatchers = append(podMatchers, matcher.ResourceMatcher{}) + } + if enablePodEnvMatcher { + podMatchers = append(podMatchers, matcher.EnvVarMatcher{}) + } + if enablePodPortMatcher { + podMatchers = append(podMatchers, matcher.PortMatcher{}) + } + if enablePodEphemeralStorageMatcher { + podMatchers = append(podMatchers, matcher.EphemeralStorageMatcher{}) + } + if err = (&controller.DevboxReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - CommitImageRegistry: registryAddr, - Recorder: mgr.GetEventRecorderFor("devbox-controller"), - RequestCPURate: requestCPURate, - RequestMemoryRate: requestMemoryRate, - RequestEphemeralStorage: requestEphemeralStorage, - LimitEphemeralStorage: limitEphemeralStorage, - DebugMode: debugMode, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + CommitImageRegistry: registryAddr, + Recorder: mgr.GetEventRecorderFor("devbox-controller"), + RequestRate: utilresource.RequestRate{ + CPU: requestCPURate, + Memory: requestMemoryRate, + }, + EphemeralStorage: utilresource.EphemeralStorage{ + DefaultRequest: resource.MustParse(requestEphemeralStorage), + DefaultLimit: resource.MustParse(limitEphemeralStorage), + MaximumLimit: resource.MustParse(maximumLimitEphemeralStorage), + }, + PodMatchers: podMatchers, + DebugMode: debugMode, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Devbox") os.Exit(1) diff --git a/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml b/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml index eaf11b9e5770..0a0d64cb4672 100644 --- a/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml +++ b/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml @@ -32,12 +32,6 @@ spec: - jsonPath: .spec.state name: State type: string - - jsonPath: .spec.runtimeRef.name - name: RuntimeRef - type: string - - jsonPath: .status.podPhase - name: PodPhase - type: string - jsonPath: .status.network.type name: NetworkType type: string @@ -834,1780 +828,1933 @@ spec: type: array type: object type: object - args: - items: - type: string - type: array - command: - items: - type: string - type: array - extraAnnotations: - additionalProperties: - type: string - type: object - extraEnvs: - description: todo add rewrite env... - items: - description: EnvVar represents an environment variable present in - a Container. - properties: - name: - description: Name of the environment variable. Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". + config: + properties: + annotations: + additionalProperties: type: string - valueFrom: - description: Source for the environment variable's value. Cannot - be used if value is not empty. + type: object + appPorts: + default: + - name: devbox-app-port + port: 8080 + protocol: TCP + items: + description: ServicePort contains information on service's port. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + appProtocol: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is - written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified - API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + The application protocol for this port. + This is used as a hint for implementations to offer richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: + + + * Un-prefixed protocol names - reserved for IANA standard service names (as per + RFC-6335 and https://www.iana.org/assignments/service-names). + + + * Kubernetes-defined prefixed names: + * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 + * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 + + + * Other protocols should use implementation-defined prefixed names such as + mycompany.com/my-custom-protocol. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed - resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace + The name of this port within the service. This must be a DNS_LABEL. + All ports within a ServiceSpec must have unique names. When considering + the endpoints for a Service, this must match the 'name' field in the + EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: |- + The port on each node on which this service is exposed when type is + NodePort or LoadBalancer. Usually assigned by the system. If a value is + specified, in-range, and not in use it will be used, otherwise the + operation will fail. If not specified, a port will be allocated if this + Service requires one. If this field is specified when creating a + Service which does not need it, creation will fail. This field will be + wiped when updating a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). + More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: |- + The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". + Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the pods targeted by the service. + Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named port in the + target Pod's container ports. If this is not specified, the value + of the 'port' field is used (an identity map). + This field is ignored for services with clusterIP=None, and should be + omitted or set equal to the 'port' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + args: + description: kubebuilder:validation:Optional + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret or its key must - be defined - type: boolean - required: - - key + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-map-type: atomic + required: + - name type: object - required: - - name - type: object - type: array - extraLabels: - additionalProperties: - type: string - description: todo add rewrite labels and annotations... - type: object - extraVolumeMounts: - items: - description: VolumeMount describes a mounting of a Volume within - a container. - properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - When not set, MountPropagationNone is used. - This field is beta in 1.10. - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - subPath: - description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: |- - Expanded path within the volume from which the container's volume should be mounted. - Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. - Defaults to "" (volume's root). - SubPathExpr and SubPath are mutually exclusive. + type: array + labels: + additionalProperties: type: string - required: - - mountPath - - name - type: object - type: array - extraVolumes: - description: todo add rewrite volumes and volume mounts.. - items: - description: Volume represents a named volume in a pod that may - be accessed by any container in the pod. - properties: - awsElasticBlockStore: - description: |- - awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: object + ports: + default: + - containerPort: 22 + name: devbox-ssh-port + protocol: TCP + description: 'TODO: in v1alpha2 api we need fix the port and app + port into one field and create a new type for it.' + items: + description: ContainerPort represents a network port in a single + container. properties: - fsType: + containerPort: description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - TODO: how do we prevent errors in the filesystem from compromising the machine + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. type: string - partition: + hostPort: description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - Examples: For volume /dev/sda1, you specify the partition as "1". - Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. format: int32 type: integer - readOnly: + name: description: |- - readOnly value true will force the readOnly setting in VolumeMounts. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - type: boolean - volumeID: + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP description: |- - volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". type: string required: - - volumeID + - containerPort type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount on - the host and bind mount to the pod. + type: array + releaseArgs: + default: + - /home/devbox/project/entrypoint.sh + items: + type: string + type: array + releaseCommand: + default: + - /bin/bash + - -c + items: + type: string + type: array + user: + default: devbox + type: string + volumeMounts: + items: + description: VolumeMount describes a mounting of a Volume within + a container. properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: None, - Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in the - blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the blob - storage + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. type: string - fsType: + mountPropagation: description: |- - fsType is Filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. type: string - kind: - description: 'kind expected values are Shared: multiple - blob disks per storage account Dedicated: single blob - disk per storage account Managed: azure managed data - disk (only in managed availability set). defaults to shared' + name: + description: This must match the Name of a Volume. type: string readOnly: description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service mount - on the host and bind mount to the pod. - properties: - readOnly: + subPath: description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that contains - Azure Storage Account Name and Key + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). type: string - shareName: - description: shareName is the azure share Name + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. type: string required: - - secretName - - shareName + - mountPath + - name type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host that - shares a pod's lifetime + type: array + volumes: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. properties: - monitors: - description: |- - monitors is Required: Monitors is a collection of Ceph monitors - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - items: - type: string - type: array - path: - description: 'path is Optional: Used as the mounted root, - rather than the full Ceph tree, default is /' - type: string - readOnly: + awsElasticBlockStore: description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: boolean - secretFile: - description: |- - secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: string - secretRef: - description: |- - secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore properties: - name: + fsType: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem from compromising the machine + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore type: string + required: + - volumeID type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is optional: User is the rados user name, default is admin - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: string - required: - - monitors - type: object - cinder: - description: |- - cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: boolean - secretRef: - description: |- - secretRef is optional: points to a secret object containing parameters used to connect - to OpenStack. + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. properties: - name: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure managed + data disk (only in managed availability set). defaults + to shared' type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI type: object - x-kubernetes-map-type: atomic - volumeID: - description: |- - volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: string - required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should populate - this volume - properties: - defaultMode: - description: |- - defaultMode is optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the ConfigMap, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional specify whether the ConfigMap or its - keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - csi: - description: csi (Container Storage Interface) represents ephemeral - storage that is handled by certain external CSI drivers (Beta - feature). - properties: - driver: - description: |- - driver is the name of the CSI driver that handles this volume. - Consult with your admin for the correct name as registered in the cluster. - type: string - fsType: - description: |- - fsType to mount. Ex. "ext4", "xfs", "ntfs". - If not provided, the empty value is passed to the associated CSI driver - which will determine the default filesystem to apply. - type: string - nodePublishSecretRef: - description: |- - nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to complete the CSI - NodePublishVolume and NodeUnpublishVolume calls. - This field is optional, and may be empty if no secret is required. If the - secret object contains more than one secret, all secret references are passed. + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. properties: - name: + readOnly: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName type: object - x-kubernetes-map-type: atomic - readOnly: - description: |- - readOnly specifies a read-only configuration for the volume. - Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: |- - volumeAttributes stores driver-specific properties that are passed to the CSI - driver. Consult your driver's documentation for supported values. + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the pod - that should populate this volume - properties: - defaultMode: + cinder: description: |- - Optional: mode bits to use on created files by default. Must be a - Optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: Items is a list of downward API volume file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field - properties: - fieldRef: - description: 'Required: Selects a field of the pod: - only annotations, labels, name and namespace are - supported.' + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + key: + description: key is the key to project. type: string - fieldPath: - description: Path of the field to select in the - specified API version. + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. type: string required: - - fieldPath + - key + - path type: object - x-kubernetes-map-type: atomic - mode: - description: |- - Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: 'Required: Path is the relative path - name of the file to be created. Must not be absolute - or contain the ''..'' path. Must be utf-8 encoded. - The first item of the relative path must not start - with ''..''' + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: type: string - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name and namespace + are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path. Must + be utf-8 encoded. The first item of the relative + path must not start with ''..''' type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic required: - - resource + - path type: object - x-kubernetes-map-type: atomic - required: - - path - type: object - type: array - type: object - emptyDir: - description: |- - emptyDir represents a temporary directory that shares a pod's lifetime. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - properties: - medium: + type: array + type: object + emptyDir: description: |- - medium represents what type of storage medium should back this directory. - The default is "" which means to use the node's default medium. - Must be an empty string (default) or Memory. + emptyDir represents a temporary directory that shares a pod's lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - type: string - sizeLimit: - anyOf: - - type: integer - - type: string + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: description: |- - sizeLimit is the total amount of local storage required for this EmptyDir volume. - The size limit is also applicable for memory medium. - The maximum usage on memory medium EmptyDir would be the minimum value between - the SizeLimit specified here and the sum of memory limits of all containers in a pod. - The default is nil which means that the limit is undefined. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - ephemeral: - description: |- - ephemeral represents a volume that is handled by a cluster storage driver. - The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, - and deleted when the pod is removed. + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. - Use this if: - a) the volume is only needed while the pod runs, - b) features of normal volumes like restoring from snapshot or capacity - tracking are needed, - c) the storage driver is specified through a storage class, and - d) the storage driver supports dynamic volume provisioning through - a PersistentVolumeClaim (see EphemeralVolumeSource for more - information on the connection between this volume type - and PersistentVolumeClaim). + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). - Use PersistentVolumeClaim or one of the vendor-specific - APIs for volumes that persist for longer than the lifecycle - of an individual pod. + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. - Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to - be used that way - see the documentation of the driver for - more information. + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. - A pod can use both types of ephemeral volumes and - persistent volumes at the same time. - properties: - volumeClaimTemplate: - description: |- - Will be used to create a stand-alone PVC to provision the volume. - The pod in which this EphemeralVolumeSource is embedded will be the - owner of the PVC, i.e. the PVC will be deleted together with the - pod. The name of the PVC will be `-` where - `` is the name from the `PodSpec.Volumes` array - entry. Pod validation will reject the pod if the concatenated name - is not valid for a PVC (for example, too long). + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `-` where + `` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). - An existing PVC with that name that is not owned by the pod - will *not* be used for the pod to avoid using an unrelated - volume by mistake. Starting the pod is then blocked until - the unrelated PVC is removed. If such a pre-created PVC is - meant to be used by the pod, the PVC has to updated with an - owner reference to the pod once the pod exists. Normally - this should not be necessary, but it may be useful when - manually reconstructing a broken cluster. + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. - This field is read-only and no changes will be made by Kubernetes - to the PVC after it has been created. + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. - Required, must not be nil. - properties: - metadata: - description: |- - May contain labels and annotations that will be copied into the PVC - when creating it. No other fields are allowed and will be rejected during - validation. - type: object - spec: - description: |- - The specification for the PersistentVolumeClaim. The entire content is - copied unchanged into the PVC that gets created from this - template. The same fields as in a PersistentVolumeClaim - are also valid here. + Required, must not be nil. properties: - accessModes: - description: |- - accessModes contains the desired access modes the volume should have. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 - items: - type: string - type: array - dataSource: + metadata: description: |- - dataSource field can be used to specify either: - * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) - If the provisioner or an external controller can support the specified data source, - it will create a new volume based on the contents of the specified data source. - When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, - and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. - If the namespace is specified, then dataSourceRef will not be copied to dataSource. - properties: - apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - required: - - kind - - name + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. type: object - x-kubernetes-map-type: atomic - dataSourceRef: + spec: description: |- - dataSourceRef specifies the object from which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a non-empty API group (non - core object) or a PersistentVolumeClaim object. - When this field is specified, volume binding will only succeed if the type of - the specified object matches some installed volume populator or dynamic - provisioner. - This field will replace the functionality of the dataSource field and as such - if both fields are non-empty, they must have the same value. For backwards - compatibility, when namespace isn't specified in dataSourceRef, - both fields (dataSource and dataSourceRef) will be set to the same - value automatically if one of them is empty and the other is non-empty. - When namespace is specified in dataSourceRef, - dataSource isn't set to the same value and must be empty. - There are three important differences between dataSource and dataSourceRef: - * While dataSource only allows two specific types of objects, dataSourceRef - allows any non-core object, as well as PersistentVolumeClaim objects. - * While dataSource ignores disallowed values (dropping them), dataSourceRef - preserves all values, and generates an error if a disallowed value is - specified. - * While dataSource only allows local objects, dataSourceRef allows objects - in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. - (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. properties: - apiGroup: + accessModes: description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - namespace: + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: description: |- - Namespace is the namespace of resource being referenced - Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. - type: string - required: - - kind - - name - type: object - resources: - description: |- - resources represents the minimum resources the volume should have. - If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements - that are lower than previous value but must still be higher than capacity recorded in the - status field of the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources - properties: - claims: + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one - entry in PodSpec.ResourceClaims. - properties: - name: + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references + one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + x-kubernetes-map-type: atomic + storageClassName: description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string type: object - selector: - description: selector is a label query over volumes - to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - storageClassName: - description: |- - storageClassName is the name of the StorageClass required by the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 - type: string - volumeMode: + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + TODO: how do we prevent errors in the filesystem from compromising the machine + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: description: |- - volumeMode defines what type of volume is required by the claim. - Value of Filesystem is implied when not included in claim spec. - type: string - volumeName: - description: volumeName is the binding reference - to the PersistentVolume backing this claim. + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? type: string type: object + x-kubernetes-map-type: atomic required: - - spec - type: object - type: object - fc: - description: fc represents a Fibre Channel resource that is - attached to a kubelet's host machine and then exposed to the - pod. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - wwids: - description: |- - wwids Optional: FC volume world wide identifiers (wwids) - Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. - items: - type: string - type: array - type: object - flexVolume: - description: |- - flexVolume represents a generic volume resource that is - provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use for - this volume. - type: string - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds extra - command options if any.' + - driver type: object - readOnly: - description: |- - readOnly is Optional: defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugin scripts. This may be - empty if no secret object is specified. If the secret object - contains more than one secret, all secrets are passed to the plugin - scripts. + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running properties: - name: + datasetName: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset type: string type: object - x-kubernetes-map-type: atomic - required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached to - a kubelet's host machine. This depends on the Flocker control - service being running - properties: - datasetName: - description: |- - datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as deprecated - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. This - is unique identifier of a Flocker dataset - type: string - type: object - gcePersistentDisk: - description: |- - gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - properties: - fsType: - description: |- - fsType is filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - partition: - description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - Examples: For volume /dev/sda1, you specify the partition as "1". - Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - format: int32 - type: integer - pdName: - description: |- - pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - type: string - readOnly: + gcePersistentDisk: description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - type: boolean - required: - - pdName - type: object - gitRepo: - description: |- - gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an - EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir - into the Pod's container. - properties: - directory: - description: |- - directory is the target directory name. - Must not contain or start with '..'. If '.' is supplied, the volume directory will be the - git repository. Otherwise, if specified, the volume will contain the git repository in - the subdirectory with the given name. - type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s.io/volumes/glusterfs/README.md - properties: - endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: string - path: - description: |- - path is the Glusterfs volume path. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: string - readOnly: + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem from compromising the machine + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: description: |- - readOnly here will force the Glusterfs volume to be mounted with read-only permissions. - Defaults to false. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: boolean - required: - - endpoints - - path - type: object - hostPath: - description: |- - hostPath represents a pre-existing file or directory on the host - machine that is directly exposed to the container. This is generally - used for system agents or other privileged things that are allowed - to see the host machine. Most containers will NOT need this. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - --- - TODO(jonesdl) We need to restrict who can use host directory mounts and who can/can not - mount host directories as read/write. - properties: - path: + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: description: |- - path of the directory on the host. - If the path is a symlink, it will follow the link to the real path. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - type: string - type: + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: description: |- - type for HostPath Volume - Defaults to "" + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - type: string - required: - - path - type: object - iscsi: - description: |- - iscsi represents an ISCSI Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://examples.k8s.io/volumes/iscsi/README.md - properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support iSCSI - Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support iSCSI - Session CHAP authentication - type: boolean - fsType: - description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - initiatorName: - description: |- - initiatorName is the custom iSCSI Initiator Name. - If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface - : will be created for the connection. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. - type: string - iscsiInterface: - description: |- - iscsiInterface is the interface Name that uses an iSCSI transport. - Defaults to 'default' (tcp). - type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: |- - portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port - is other than default (typically TCP ports 860 and 3260). - items: - type: string - type: array - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI target - and initiator authentication + --- + TODO(jonesdl) We need to restrict who can use host directory mounts and who can/can not + mount host directories as read/write. properties: - name: + path: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath type: string + required: + - path type: object - x-kubernetes-map-type: atomic - targetPortal: + iscsi: description: |- - targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port - is other than default (typically TCP ports 860 and 3260). - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: |- - name of the volume. - Must be a DNS_LABEL and unique within the pod. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - nfs: - description: |- - nfs represents an NFS mount on the host that shares a pod's lifetime - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - properties: - path: + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem from compromising the machine + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + : will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: description: |- - path that is exported by the NFS server. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string - readOnly: + nfs: description: |- - readOnly here will force the NFS export to be mounted with read-only permissions. - Defaults to false. + nfs represents an NFS mount on the host that shares a pod's lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: boolean - server: - description: |- - server is the hostname or IP address of the NFS server. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: |- - persistentVolumeClaimVolumeSource represents a reference to a - PersistentVolumeClaim in the same namespace. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims - properties: - claimName: + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: description: |- - claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims - type: string - readOnly: - description: |- - readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean - required: - - claimName - type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID - type: object - portworxVolume: - description: portworxVolume represents a portworx volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx volume - type: string - required: - - volumeID - type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API - properties: - defaultMode: - description: |- - defaultMode are the mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along with - other supported volume types - properties: - configMap: - description: configMap information about the configMap - data to project - properties: - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the ConfigMap, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - downwardAPI: - description: downwardAPI information about the downwardAPI - data to project + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected along + with other supported volume types properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing - the pod field - properties: - fieldRef: - description: 'Required: Selects a field - of the pod: only annotations, labels, - name and namespace are supported.' + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 + encoded. The first item of the relative + path must not start with ''..''' type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic required: - - fieldPath + - path type: object - x-kubernetes-map-type: atomic - mode: - description: |- - Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must - not be absolute or contain the ''..'' - path. Must be utf-8 encoded. The first - item of the relative path must not start - with ''..''' - type: string - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + type: array + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' + key: + description: key is the key to project. type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. type: string required: - - resource + - key + - path type: object - x-kubernetes-map-type: atomic - required: - - path - type: object - type: array - type: object - secret: - description: secret information about the secret data - to project - properties: - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the Secret, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional field specify whether the - Secret or its key must be defined - type: boolean + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object type: object - x-kubernetes-map-type: atomic - serviceAccountToken: - description: serviceAccountToken is information about - the serviceAccountToken data to project + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem from compromising the machine + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. properties: - audience: - description: |- - audience is the intended audience of the token. A recipient of a token - must identify itself with an identifier specified in the audience of the - token, and otherwise should reject the token. The audience defaults to the - identifier of the apiserver. + key: + description: key is the key to project. type: string - expirationSeconds: + mode: description: |- - expirationSeconds is the requested duration of validity of the service - account token. As the token approaches expiration, the kubelet volume - plugin will proactively rotate the service account token. The kubelet will - start trying to rotate the token if the token is older than 80 percent of - its time to live or if the token is older than 24 hours.Defaults to 1 hour - and must be at least 10 minutes. - format: int64 + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 type: integer path: description: |- - path is the path relative to the mount point of the file to project the - token into. + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. type: string required: + - key - path type: object - type: object - type: array - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime - properties: - group: - description: |- - group to map volume access to - Default is no group - type: string - readOnly: - description: |- - readOnly here will force the Quobyte volume to be mounted with read-only permissions. - Defaults to false. - type: boolean - registry: - description: |- - registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple entries are separated with commas) - which acts as the central registry for volumes - type: string - tenant: - description: |- - tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by the plugin - type: string - user: - description: |- - user to map volume access to - Defaults to serivceaccount user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s.io/volumes/rbd/README.md - properties: - fsType: - description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - image: - description: |- - image is the rados image name. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - keyring: - description: |- - keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - monitors: - description: |- - monitors is a collection of Ceph monitors. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - items: - type: string - type: array - pool: - description: |- - pool is the rados pool name. - Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: boolean - secretRef: - description: |- - secretRef is name of the authentication secret for RBDUser. If provided - overrides keyring. - Default is nil. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - properties: - name: + type: array + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret type: string type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is the rados user name. - Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume - attached and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". - Default is "xfs". - type: string - gateway: - description: gateway is the host address of the ScaleIO - API Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO - Protection Domain for the configured storage. - type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef references to the secret for ScaleIO user and other - sensitive information. If this is not provided, Login operation will fail. + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. properties: - name: + fsType: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. type: string type: object - x-kubernetes-map-type: atomic - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: |- - storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. - Default is ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool associated - with the protection domain. - type: string - system: - description: system is the name of the storage system as - configured in ScaleIO. - type: string - volumeName: - description: |- - volumeName is the name of a volume already created in the ScaleIO system - that is associated with this volume source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: |- - secret represents a secret that should populate this volume. - More info: https://kubernetes.io/docs/concepts/storage/volumes#secret - properties: - defaultMode: - description: |- - defaultMode is Optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values - for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: |- - items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the Secret, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - optional: - description: optional field specify whether the Secret or - its keys must be defined - type: boolean - secretName: - description: |- - secretName is the name of the secret in the pod's namespace to use. - More info: https://kubernetes.io/docs/concepts/storage/volumes#secret - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef specifies the secret to use for obtaining the StorageOS API - credentials. If not specified, default values will be attempted. + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine properties: - name: + fsType: description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath type: object - x-kubernetes-map-type: atomic - volumeName: - description: |- - volumeName is the human-readable name of the StorageOS volume. Volume - names are only unique within a namespace. - type: string - volumeNamespace: - description: |- - volumeNamespace specifies the scope of the volume within StorageOS. If no - namespace is specified then the Pod's namespace will be used. This allows the - Kubernetes name scoping to be mirrored within StorageOS for tighter integration. - Set VolumeName to any name to override the default behaviour. - Set to "default" if you are not using namespaces within StorageOS. - Namespaces that do not pre-exist within StorageOS will be created. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based - Management (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy Based - Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies vSphere - volume vmdk - type: string required: - - volumePath + - name type: object - required: - - name - type: object - type: array + type: array + workingDir: + default: /home/devbox/project + type: string + type: object + image: + type: string network: properties: extraPorts: @@ -2656,6 +2803,10 @@ spec: required: - type type: object + nodeSelector: + additionalProperties: + type: string + type: object resource: additionalProperties: anyOf: @@ -2663,16 +2814,10 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name, quantity) pairs. type: object - runtimeRef: - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + runtimeClassName: + type: string squash: default: false type: boolean @@ -2681,6 +2826,8 @@ spec: - Running - Stopped type: string + templateID: + type: string tolerations: items: description: |- @@ -2719,11 +2866,10 @@ spec: type: string type: object type: array - workingDir: - type: string required: + - config + - image - resource - - runtimeRef - state type: object status: diff --git a/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimeclasses.yaml b/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimeclasses.yaml deleted file mode 100644 index 29513a0b9b83..000000000000 --- a/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimeclasses.yaml +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: runtimeclasses.devbox.sealos.io -spec: - group: devbox.sealos.io - names: - kind: RuntimeClass - listKind: RuntimeClassList - plural: runtimeclasses - singular: runtimeclass - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: RuntimeClass is the Schema for the runtimeclasses API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: RuntimeClassSpec defines the desired state of RuntimeClass - properties: - description: - type: string - kind: - enum: - - OS - - Language - - Framework - type: string - title: - type: string - required: - - kind - - title - type: object - status: - description: RuntimeClassStatus defines the observed state of RuntimeClass - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimes.yaml b/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimes.yaml deleted file mode 100644 index 00ed31895c33..000000000000 --- a/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimes.yaml +++ /dev/null @@ -1,2040 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: runtimes.devbox.sealos.io -spec: - group: devbox.sealos.io - names: - kind: Runtime - listKind: RuntimeList - plural: runtimes - singular: runtime - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.classRef - name: Class - type: string - - jsonPath: .spec.version - name: Version - type: string - - jsonPath: .spec.runtimeVersion - name: RuntimeVersion - type: string - - jsonPath: .spec.state - name: State - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: Runtime is the Schema for the runtimes API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: RuntimeSpec defines the desired state of Runtime - properties: - category: - items: - type: string - type: array - classRef: - type: string - components: - items: - properties: - name: - type: string - version: - type: string - required: - - name - - version - type: object - type: array - config: - properties: - annotations: - additionalProperties: - type: string - type: object - appPorts: - default: - - name: devbox-app-port - port: 8080 - protocol: TCP - items: - description: ServicePort contains information on service's port. - properties: - appProtocol: - description: |- - The application protocol for this port. - This is used as a hint for implementations to offer richer behavior for protocols that they understand. - This field follows standard Kubernetes label syntax. - Valid values are either: - - - * Un-prefixed protocol names - reserved for IANA standard service names (as per - RFC-6335 and https://www.iana.org/assignments/service-names). - - - * Kubernetes-defined prefixed names: - * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 - * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 - * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 - - - * Other protocols should use implementation-defined prefixed names such as - mycompany.com/my-custom-protocol. - type: string - name: - description: |- - The name of this port within the service. This must be a DNS_LABEL. - All ports within a ServiceSpec must have unique names. When considering - the endpoints for a Service, this must match the 'name' field in the - EndpointPort. - Optional if only one ServicePort is defined on this service. - type: string - nodePort: - description: |- - The port on each node on which this service is exposed when type is - NodePort or LoadBalancer. Usually assigned by the system. If a value is - specified, in-range, and not in use it will be used, otherwise the - operation will fail. If not specified, a port will be allocated if this - Service requires one. If this field is specified when creating a - Service which does not need it, creation will fail. This field will be - wiped when updating a Service to no longer need it (e.g. changing type - from NodePort to ClusterIP). - More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport - format: int32 - type: integer - port: - description: The port that will be exposed by this service. - format: int32 - type: integer - protocol: - default: TCP - description: |- - The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". - Default is TCP. - type: string - targetPort: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the pods targeted by the service. - Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. - If this is a string, it will be looked up as a named port in the - target Pod's container ports. If this is not specified, the value - of the 'port' field is used (an identity map). - This field is ignored for services with clusterIP=None, and should be - omitted or set equal to the 'port' field. - More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service - x-kubernetes-int-or-string: true - required: - - port - type: object - type: array - args: - description: kubebuilder:validation:Optional - items: - type: string - type: array - command: - items: - type: string - type: array - env: - items: - description: EnvVar represents an environment variable present - in a Container. - properties: - name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. - Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's - namespace - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - type: string - labels: - additionalProperties: - type: string - type: object - ports: - default: - - containerPort: 22 - name: devbox-ssh-port - protocol: TCP - description: 'TODO: in v1alpha2 api we need fix the port and app - port into one field and create a new type for it.' - items: - description: ContainerPort represents a network port in a single - container. - properties: - containerPort: - description: |- - Number of port to expose on the pod's IP address. - This must be a valid port number, 0 < x < 65536. - format: int32 - type: integer - hostIP: - description: What host IP to bind the external port to. - type: string - hostPort: - description: |- - Number of port to expose on the host. - If specified, this must be a valid port number, 0 < x < 65536. - If HostNetwork is specified, this must match ContainerPort. - Most containers do not need this. - format: int32 - type: integer - name: - description: |- - If specified, this must be an IANA_SVC_NAME and unique within the pod. Each - named port in a pod must have a unique name. Name for the port that can be - referred to by services. - type: string - protocol: - default: TCP - description: |- - Protocol for port. Must be UDP, TCP, or SCTP. - Defaults to "TCP". - type: string - required: - - containerPort - type: object - type: array - releaseArgs: - default: - - /home/sealos/project/entrypoint.sh - items: - type: string - type: array - releaseCommand: - default: - - /bin/bash - - -c - items: - type: string - type: array - user: - default: sealos - type: string - volumeMounts: - items: - description: VolumeMount describes a mounting of a Volume within - a container. - properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - When not set, MountPropagationNone is used. - This field is beta in 1.10. - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - subPath: - description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: |- - Expanded path within the volume from which the container's volume should be mounted. - Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. - Defaults to "" (volume's root). - SubPathExpr and SubPath are mutually exclusive. - type: string - required: - - mountPath - - name - type: object - type: array - volumes: - items: - description: Volume represents a named volume in a pod that - may be accessed by any container in the pod. - properties: - awsElasticBlockStore: - description: |- - awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - properties: - fsType: - description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - partition: - description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - Examples: For volume /dev/sda1, you specify the partition as "1". - Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). - format: int32 - type: integer - readOnly: - description: |- - readOnly value true will force the readOnly setting in VolumeMounts. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - type: boolean - volumeID: - description: |- - volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - type: string - required: - - volumeID - type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount - on the host and bind mount to the pod. - properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: - None, Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in - the blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the - blob storage - type: string - fsType: - description: |- - fsType is Filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - kind: - description: 'kind expected values are Shared: multiple - blob disks per storage account Dedicated: single - blob disk per storage account Managed: azure managed - data disk (only in managed availability set). defaults - to shared' - type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service - mount on the host and bind mount to the pod. - properties: - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that - contains Azure Storage Account Name and Key - type: string - shareName: - description: shareName is the azure share Name - type: string - required: - - secretName - - shareName - type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime - properties: - monitors: - description: |- - monitors is Required: Monitors is a collection of Ceph monitors - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - items: - type: string - type: array - path: - description: 'path is Optional: Used as the mounted - root, rather than the full Ceph tree, default is /' - type: string - readOnly: - description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: boolean - secretFile: - description: |- - secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: string - secretRef: - description: |- - secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is optional: User is the rados user name, default is admin - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: string - required: - - monitors - type: object - cinder: - description: |- - cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: boolean - secretRef: - description: |- - secretRef is optional: points to a secret object containing parameters used to connect - to OpenStack. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - volumeID: - description: |- - volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: string - required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should - populate this volume - properties: - defaultMode: - description: |- - defaultMode is optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the ConfigMap, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within a - volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - csi: - description: csi (Container Storage Interface) represents - ephemeral storage that is handled by certain external - CSI drivers (Beta feature). - properties: - driver: - description: |- - driver is the name of the CSI driver that handles this volume. - Consult with your admin for the correct name as registered in the cluster. - type: string - fsType: - description: |- - fsType to mount. Ex. "ext4", "xfs", "ntfs". - If not provided, the empty value is passed to the associated CSI driver - which will determine the default filesystem to apply. - type: string - nodePublishSecretRef: - description: |- - nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to complete the CSI - NodePublishVolume and NodeUnpublishVolume calls. - This field is optional, and may be empty if no secret is required. If the - secret object contains more than one secret, all secret references are passed. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - readOnly: - description: |- - readOnly specifies a read-only configuration for the volume. - Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: |- - volumeAttributes stores driver-specific properties that are passed to the CSI - driver. Consult your driver's documentation for supported values. - type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the - pod that should populate this volume - properties: - defaultMode: - description: |- - Optional: mode bits to use on created files by default. Must be a - Optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: Items is a list of downward API volume - file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field - properties: - fieldRef: - description: 'Required: Selects a field of the - pod: only annotations, labels, name and namespace - are supported.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in - the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - mode: - description: |- - Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must not - be absolute or contain the ''..'' path. Must - be utf-8 encoded. The first item of the relative - path must not start with ''..''' - type: string - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. - properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of - the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - required: - - path - type: object - type: array - type: object - emptyDir: - description: |- - emptyDir represents a temporary directory that shares a pod's lifetime. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - properties: - medium: - description: |- - medium represents what type of storage medium should back this directory. - The default is "" which means to use the node's default medium. - Must be an empty string (default) or Memory. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - type: string - sizeLimit: - anyOf: - - type: integer - - type: string - description: |- - sizeLimit is the total amount of local storage required for this EmptyDir volume. - The size limit is also applicable for memory medium. - The maximum usage on memory medium EmptyDir would be the minimum value between - the SizeLimit specified here and the sum of memory limits of all containers in a pod. - The default is nil which means that the limit is undefined. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - ephemeral: - description: |- - ephemeral represents a volume that is handled by a cluster storage driver. - The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, - and deleted when the pod is removed. - - - Use this if: - a) the volume is only needed while the pod runs, - b) features of normal volumes like restoring from snapshot or capacity - tracking are needed, - c) the storage driver is specified through a storage class, and - d) the storage driver supports dynamic volume provisioning through - a PersistentVolumeClaim (see EphemeralVolumeSource for more - information on the connection between this volume type - and PersistentVolumeClaim). - - - Use PersistentVolumeClaim or one of the vendor-specific - APIs for volumes that persist for longer than the lifecycle - of an individual pod. - - - Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to - be used that way - see the documentation of the driver for - more information. - - - A pod can use both types of ephemeral volumes and - persistent volumes at the same time. - properties: - volumeClaimTemplate: - description: |- - Will be used to create a stand-alone PVC to provision the volume. - The pod in which this EphemeralVolumeSource is embedded will be the - owner of the PVC, i.e. the PVC will be deleted together with the - pod. The name of the PVC will be `-` where - `` is the name from the `PodSpec.Volumes` array - entry. Pod validation will reject the pod if the concatenated name - is not valid for a PVC (for example, too long). - - - An existing PVC with that name that is not owned by the pod - will *not* be used for the pod to avoid using an unrelated - volume by mistake. Starting the pod is then blocked until - the unrelated PVC is removed. If such a pre-created PVC is - meant to be used by the pod, the PVC has to updated with an - owner reference to the pod once the pod exists. Normally - this should not be necessary, but it may be useful when - manually reconstructing a broken cluster. - - - This field is read-only and no changes will be made by Kubernetes - to the PVC after it has been created. - - - Required, must not be nil. - properties: - metadata: - description: |- - May contain labels and annotations that will be copied into the PVC - when creating it. No other fields are allowed and will be rejected during - validation. - type: object - spec: - description: |- - The specification for the PersistentVolumeClaim. The entire content is - copied unchanged into the PVC that gets created from this - template. The same fields as in a PersistentVolumeClaim - are also valid here. - properties: - accessModes: - description: |- - accessModes contains the desired access modes the volume should have. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 - items: - type: string - type: array - dataSource: - description: |- - dataSource field can be used to specify either: - * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) - If the provisioner or an external controller can support the specified data source, - it will create a new volume based on the contents of the specified data source. - When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, - and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. - If the namespace is specified, then dataSourceRef will not be copied to dataSource. - properties: - apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource - being referenced - type: string - name: - description: Name is the name of resource - being referenced - type: string - required: - - kind - - name - type: object - x-kubernetes-map-type: atomic - dataSourceRef: - description: |- - dataSourceRef specifies the object from which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a non-empty API group (non - core object) or a PersistentVolumeClaim object. - When this field is specified, volume binding will only succeed if the type of - the specified object matches some installed volume populator or dynamic - provisioner. - This field will replace the functionality of the dataSource field and as such - if both fields are non-empty, they must have the same value. For backwards - compatibility, when namespace isn't specified in dataSourceRef, - both fields (dataSource and dataSourceRef) will be set to the same - value automatically if one of them is empty and the other is non-empty. - When namespace is specified in dataSourceRef, - dataSource isn't set to the same value and must be empty. - There are three important differences between dataSource and dataSourceRef: - * While dataSource only allows two specific types of objects, dataSourceRef - allows any non-core object, as well as PersistentVolumeClaim objects. - * While dataSource ignores disallowed values (dropping them), dataSourceRef - preserves all values, and generates an error if a disallowed value is - specified. - * While dataSource only allows local objects, dataSourceRef allows objects - in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. - (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. - properties: - apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource - being referenced - type: string - name: - description: Name is the name of resource - being referenced - type: string - namespace: - description: |- - Namespace is the namespace of resource being referenced - Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. - type: string - required: - - kind - - name - type: object - resources: - description: |- - resources represents the minimum resources the volume should have. - If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements - that are lower than previous value but must still be higher than capacity recorded in the - status field of the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references - one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - selector: - description: selector is a label query over - volumes to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - storageClassName: - description: |- - storageClassName is the name of the StorageClass required by the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 - type: string - volumeMode: - description: |- - volumeMode defines what type of volume is required by the claim. - Value of Filesystem is implied when not included in claim spec. - type: string - volumeName: - description: volumeName is the binding reference - to the PersistentVolume backing this claim. - type: string - type: object - required: - - spec - type: object - type: object - fc: - description: fc represents a Fibre Channel resource that - is attached to a kubelet's host machine and then exposed - to the pod. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - wwids: - description: |- - wwids Optional: FC volume world wide identifiers (wwids) - Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. - items: - type: string - type: array - type: object - flexVolume: - description: |- - flexVolume represents a generic volume resource that is - provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use - for this volume. - type: string - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds - extra command options if any.' - type: object - readOnly: - description: |- - readOnly is Optional: defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugin scripts. This may be - empty if no secret object is specified. If the secret object - contains more than one secret, all secrets are passed to the plugin - scripts. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached - to a kubelet's host machine. This depends on the Flocker - control service being running - properties: - datasetName: - description: |- - datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as deprecated - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. - This is unique identifier of a Flocker dataset - type: string - type: object - gcePersistentDisk: - description: |- - gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - properties: - fsType: - description: |- - fsType is filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - partition: - description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - Examples: For volume /dev/sda1, you specify the partition as "1". - Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - format: int32 - type: integer - pdName: - description: |- - pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - type: boolean - required: - - pdName - type: object - gitRepo: - description: |- - gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an - EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir - into the Pod's container. - properties: - directory: - description: |- - directory is the target directory name. - Must not contain or start with '..'. If '.' is supplied, the volume directory will be the - git repository. Otherwise, if specified, the volume will contain the git repository in - the subdirectory with the given name. - type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s.io/volumes/glusterfs/README.md - properties: - endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: string - path: - description: |- - path is the Glusterfs volume path. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: string - readOnly: - description: |- - readOnly here will force the Glusterfs volume to be mounted with read-only permissions. - Defaults to false. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: boolean - required: - - endpoints - - path - type: object - hostPath: - description: |- - hostPath represents a pre-existing file or directory on the host - machine that is directly exposed to the container. This is generally - used for system agents or other privileged things that are allowed - to see the host machine. Most containers will NOT need this. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - --- - TODO(jonesdl) We need to restrict who can use host directory mounts and who can/can not - mount host directories as read/write. - properties: - path: - description: |- - path of the directory on the host. - If the path is a symlink, it will follow the link to the real path. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - type: string - type: - description: |- - type for HostPath Volume - Defaults to "" - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - type: string - required: - - path - type: object - iscsi: - description: |- - iscsi represents an ISCSI Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://examples.k8s.io/volumes/iscsi/README.md - properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support - iSCSI Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support - iSCSI Session CHAP authentication - type: boolean - fsType: - description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - initiatorName: - description: |- - initiatorName is the custom iSCSI Initiator Name. - If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface - : will be created for the connection. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. - type: string - iscsiInterface: - description: |- - iscsiInterface is the interface Name that uses an iSCSI transport. - Defaults to 'default' (tcp). - type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: |- - portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port - is other than default (typically TCP ports 860 and 3260). - items: - type: string - type: array - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI - target and initiator authentication - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - targetPortal: - description: |- - targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port - is other than default (typically TCP ports 860 and 3260). - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: |- - name of the volume. - Must be a DNS_LABEL and unique within the pod. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - nfs: - description: |- - nfs represents an NFS mount on the host that shares a pod's lifetime - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - properties: - path: - description: |- - path that is exported by the NFS server. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: string - readOnly: - description: |- - readOnly here will force the NFS export to be mounted with read-only permissions. - Defaults to false. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: boolean - server: - description: |- - server is the hostname or IP address of the NFS server. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: |- - persistentVolumeClaimVolumeSource represents a reference to a - PersistentVolumeClaim in the same namespace. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims - properties: - claimName: - description: |- - claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims - type: string - readOnly: - description: |- - readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean - required: - - claimName - type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host - machine - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID - type: object - portworxVolume: - description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine - properties: - fsType: - description: |- - fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx - volume - type: string - required: - - volumeID - type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API - properties: - defaultMode: - description: |- - defaultMode are the mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along - with other supported volume types - properties: - configMap: - description: configMap information about the configMap - data to project - properties: - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the ConfigMap, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path - within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional specify whether the - ConfigMap or its keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - downwardAPI: - description: downwardAPI information about the - downwardAPI data to project - properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing - the pod field - properties: - fieldRef: - description: 'Required: Selects a field - of the pod: only annotations, labels, - name and namespace are supported.' - properties: - apiVersion: - description: Version of the schema - the FieldPath is written in terms - of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to - select in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - mode: - description: |- - Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: 'Required: Path is the - relative path name of the file to - be created. Must not be absolute or - contain the ''..'' path. Must be utf-8 - encoded. The first item of the relative - path must not start with ''..''' - type: string - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env - vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output - format of the exposed resources, - defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource - to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - required: - - path - type: object - type: array - type: object - secret: - description: secret information about the secret - data to project - properties: - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the Secret, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path - within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional field specify whether - the Secret or its key must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - serviceAccountToken: - description: serviceAccountToken is information - about the serviceAccountToken data to project - properties: - audience: - description: |- - audience is the intended audience of the token. A recipient of a token - must identify itself with an identifier specified in the audience of the - token, and otherwise should reject the token. The audience defaults to the - identifier of the apiserver. - type: string - expirationSeconds: - description: |- - expirationSeconds is the requested duration of validity of the service - account token. As the token approaches expiration, the kubelet volume - plugin will proactively rotate the service account token. The kubelet will - start trying to rotate the token if the token is older than 80 percent of - its time to live or if the token is older than 24 hours.Defaults to 1 hour - and must be at least 10 minutes. - format: int64 - type: integer - path: - description: |- - path is the path relative to the mount point of the file to project the - token into. - type: string - required: - - path - type: object - type: object - type: array - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime - properties: - group: - description: |- - group to map volume access to - Default is no group - type: string - readOnly: - description: |- - readOnly here will force the Quobyte volume to be mounted with read-only permissions. - Defaults to false. - type: boolean - registry: - description: |- - registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple entries are separated with commas) - which acts as the central registry for volumes - type: string - tenant: - description: |- - tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by the plugin - type: string - user: - description: |- - user to map volume access to - Defaults to serivceaccount user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s.io/volumes/rbd/README.md - properties: - fsType: - description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - image: - description: |- - image is the rados image name. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - keyring: - description: |- - keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - monitors: - description: |- - monitors is a collection of Ceph monitors. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - items: - type: string - type: array - pool: - description: |- - pool is the rados pool name. - Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: boolean - secretRef: - description: |- - secretRef is name of the authentication secret for RBDUser. If provided - overrides keyring. - Default is nil. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is the rados user name. - Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume - attached and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". - Default is "xfs". - type: string - gateway: - description: gateway is the host address of the ScaleIO - API Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO - Protection Domain for the configured storage. - type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef references to the secret for ScaleIO user and other - sensitive information. If this is not provided, Login operation will fail. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: |- - storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. - Default is ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool - associated with the protection domain. - type: string - system: - description: system is the name of the storage system - as configured in ScaleIO. - type: string - volumeName: - description: |- - volumeName is the name of a volume already created in the ScaleIO system - that is associated with this volume source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: |- - secret represents a secret that should populate this volume. - More info: https://kubernetes.io/docs/concepts/storage/volumes#secret - properties: - defaultMode: - description: |- - defaultMode is Optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values - for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: |- - items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the Secret, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within a - volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - optional: - description: optional field specify whether the Secret - or its keys must be defined - type: boolean - secretName: - description: |- - secretName is the name of the secret in the pod's namespace to use. - More info: https://kubernetes.io/docs/concepts/storage/volumes#secret - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef specifies the secret to use for obtaining the StorageOS API - credentials. If not specified, default values will be attempted. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - volumeName: - description: |- - volumeName is the human-readable name of the StorageOS volume. Volume - names are only unique within a namespace. - type: string - volumeNamespace: - description: |- - volumeNamespace specifies the scope of the volume within StorageOS. If no - namespace is specified then the Pod's namespace will be used. This allows the - Kubernetes name scoping to be mirrored within StorageOS for tighter integration. - Set VolumeName to any name to override the default behaviour. - Set to "default" if you are not using namespaces within StorageOS. - Namespaces that do not pre-exist within StorageOS will be created. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based - Management (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy - Based Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies - vSphere volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - type: array - workingDir: - default: /home/sealos/project - type: string - required: - - image - type: object - description: - type: string - runtimeVersion: - type: string - state: - default: active - enum: - - active - - deprecated - type: string - version: - type: string - required: - - classRef - - config - - version - type: object - status: - description: RuntimeStatus defines the observed state of Runtime - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controllers/devbox/config/crd/kustomization.yaml b/controllers/devbox/config/crd/kustomization.yaml index 455200e6d9e9..37bb4dbbdd96 100644 --- a/controllers/devbox/config/crd/kustomization.yaml +++ b/controllers/devbox/config/crd/kustomization.yaml @@ -17,8 +17,6 @@ # It should be run by config/default resources: - bases/devbox.sealos.io_devboxes.yaml -- bases/devbox.sealos.io_runtimes.yaml -- bases/devbox.sealos.io_runtimeclasses.yaml - bases/devbox.sealos.io_devboxreleases.yaml - bases/devbox.sealos.io_operationrequests.yaml # +kubebuilder:scaffold:crdkustomizeresource @@ -31,8 +29,6 @@ patches: # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- path: patches/cainjection_in_devboxes.yaml -#- path: patches/cainjection_in_runtimes.yaml -#- path: patches/cainjection_in_runtimeclasses.yaml #- path: patches/cainjection_in_devboxreleases.yaml #- path: patches/cainjection_in_operationrequests.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch diff --git a/controllers/devbox/config/manager/manager.yaml b/controllers/devbox/config/manager/manager.yaml index d8098831880e..a571e871f397 100644 --- a/controllers/devbox/config/manager/manager.yaml +++ b/controllers/devbox/config/manager/manager.yaml @@ -80,7 +80,6 @@ spec: - --registry-addr={{ .registryAddr }} - --registry-user={{ .registryUser }} - --registry-password={{ .registryPassword }} - - --auth-addr={{ .authAddr }} image: controller:latest name: manager securityContext: diff --git a/controllers/devbox/config/rbac/kustomization.yaml b/controllers/devbox/config/rbac/kustomization.yaml index f7feaa591dc4..1a77a2ef46e5 100644 --- a/controllers/devbox/config/rbac/kustomization.yaml +++ b/controllers/devbox/config/rbac/kustomization.yaml @@ -40,10 +40,6 @@ resources: - operationrequest_viewer_role.yaml - devboxrelease_editor_role.yaml - devboxrelease_viewer_role.yaml -- runtimeclass_editor_role.yaml -- runtimeclass_viewer_role.yaml -- runtime_editor_role.yaml -- runtime_viewer_role.yaml - devbox_editor_role.yaml - devbox_viewer_role.yaml diff --git a/controllers/devbox/config/rbac/runtime_editor_role.yaml b/controllers/devbox/config/rbac/runtime_editor_role.yaml deleted file mode 100644 index e3645875342c..000000000000 --- a/controllers/devbox/config/rbac/runtime_editor_role.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# permissions for end users to edit runtimes. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: devbox - app.kubernetes.io/managed-by: kustomize - name: runtime-editor-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimes - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimes/status - verbs: - - get diff --git a/controllers/devbox/config/rbac/runtime_viewer_role.yaml b/controllers/devbox/config/rbac/runtime_viewer_role.yaml deleted file mode 100644 index 464ff30775f1..000000000000 --- a/controllers/devbox/config/rbac/runtime_viewer_role.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# permissions for end users to view runtimes. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: devbox - app.kubernetes.io/managed-by: kustomize - name: runtime-viewer-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimes - verbs: - - get - - list - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimes/status - verbs: - - get diff --git a/controllers/devbox/config/rbac/runtimeclass_editor_role.yaml b/controllers/devbox/config/rbac/runtimeclass_editor_role.yaml deleted file mode 100644 index 9483f0bd01a9..000000000000 --- a/controllers/devbox/config/rbac/runtimeclass_editor_role.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# permissions for end users to edit runtimeclasses. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: devbox - app.kubernetes.io/managed-by: kustomize - name: runtimeclass-editor-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses/status - verbs: - - get diff --git a/controllers/devbox/config/rbac/runtimeclass_viewer_role.yaml b/controllers/devbox/config/rbac/runtimeclass_viewer_role.yaml deleted file mode 100644 index 97ba3a15d8f0..000000000000 --- a/controllers/devbox/config/rbac/runtimeclass_viewer_role.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# permissions for end users to view runtimeclasses. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: devbox - app.kubernetes.io/managed-by: kustomize - name: runtimeclass-viewer-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses - verbs: - - get - - list - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses/status - verbs: - - get diff --git a/controllers/devbox/config/samples/devbox_v1alpha1_devbox.yaml b/controllers/devbox/config/samples/devbox_v1alpha1_devbox.yaml index b0b194ef79c2..71e5bf070b02 100644 --- a/controllers/devbox/config/samples/devbox_v1alpha1_devbox.yaml +++ b/controllers/devbox/config/samples/devbox_v1alpha1_devbox.yaml @@ -18,19 +18,23 @@ metadata: labels: app.kubernetes.io/name: devbox app.kubernetes.io/managed-by: kustomize - name: devbox-sample + name: devbox-gpu-sample spec: state: Running + runtimeClassName: nvidia resource: cpu: 2 memory: 4000Mi + nvidia.com/gpu: 1 runtimeRef: - name: go-1-22-5 + name: go-1-22-5-2024-11-12-0651 namespace: devbox-system + nodeSelector: + nvidia.com/gpu.product: Tesla-P40 network: type: NodePort extraPorts: - containerPort: 443 name: 'https' - containerPort: 80 - name: 'http' \ No newline at end of file + name: 'http' diff --git a/controllers/devbox/config/samples/devbox_v1alpha1_runtime.yaml b/controllers/devbox/config/samples/devbox_v1alpha1_runtime.yaml deleted file mode 100644 index 774a7a5947ef..000000000000 --- a/controllers/devbox/config/samples/devbox_v1alpha1_runtime.yaml +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: devbox.sealos.io/v1alpha1 -kind: Runtime -metadata: - name: go-1-22-5 - namespace: devbox-system -spec: - classRef: go - title: go1.22.5 - description: go1.22.5 - config: - image: ghcr.io/cbluebird/devbox/go1.22.5:2f4067 - workingDir: /home/sealos/project - releaseCommand: - - /bin/bash - - -c - releaseArgs: - - /home/sealos/project/entrypoint.sh - category: - - ubuntu - - go ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: Runtime -metadata: - name: go-1-23-0 - namespace: devbox-system -spec: - classRef: go - config: - image: ghcr.io/labring-actions/devbox/go-1.23.0:409348 - ports: - - containerPort: 22 - name: devbox-ssh-port - protocol: TCP - appPorts: - - name: devbox-app-port - port: 8080 - protocol: TCP - user: sealos - workingDir: /home/sealos/project - releaseCommand: - - /bin/bash - - -c - releaseArgs: - - /home/sealos/project/entrypoint.sh - description: go 1.23.0 - version: "1.23.0" ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: Runtime -metadata: - name: gin - namespace: devbox-system -spec: - classRef: gin - title: gin - description: gin - config: - image: ghcr.io/cbluebird/devbox/gin:2f4067 - category: - - ubuntu - - go - - gin ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: Runtime -metadata: - name: spring-boot - namespace: devbox-system -spec: - classRef: spring-boot - title: Spring Boot - description: Spring Boot - config: - image: ghcr.io/cbluebird/devbox/spring-boot:2f4067 - category: - - ubuntu - - java - - spring-boot diff --git a/controllers/devbox/config/samples/devbox_v1alpha1_runtimeclass.yaml b/controllers/devbox/config/samples/devbox_v1alpha1_runtimeclass.yaml deleted file mode 100644 index 3886893f263a..000000000000 --- a/controllers/devbox/config/samples/devbox_v1alpha1_runtimeclass.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright © 2024 sealos. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: devbox.sealos.io/v1alpha1 -kind: RuntimeClass -metadata: - name: go - namespace: devbox-system -spec: - kind: Language - title: go - description: go ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: RuntimeClass -metadata: - name: gin - namespace: devbox-system -spec: - kind: Framework - title: gin - description: gin ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: RuntimeClass -metadata: - name: java - namespace: devbox-system -spec: - kind: Language - title: java - description: java ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: RuntimeClass -metadata: - name: spring-boot - namespace: devbox-system -spec: - kind: Framework - title: Spring Boot - description: Spring Boot ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: RuntimeClass -metadata: - name: python - namespace: devbox-system -spec: - kind: Language - title: python - description: python ---- -apiVersion: devbox.sealos.io/v1alpha1 -kind: RuntimeClass -metadata: - name: nodejs - namespace: devbox-system -spec: - kind: Language - title: node.js - description: node.js \ No newline at end of file diff --git a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl index 7286e29b5b83..910d8310bf19 100644 --- a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl @@ -40,12 +40,6 @@ spec: - jsonPath: .spec.state name: State type: string - - jsonPath: .spec.runtimeRef.name - name: RuntimeRef - type: string - - jsonPath: .status.podPhase - name: PodPhase - type: string - jsonPath: .status.network.type name: NetworkType type: string @@ -842,2488 +836,145 @@ spec: type: array type: object type: object - args: - items: - type: string - type: array - command: - items: - type: string - type: array - extraAnnotations: - additionalProperties: - type: string - type: object - extraEnvs: - description: todo add rewrite env... - items: - description: EnvVar represents an environment variable present in - a Container. - properties: - name: - description: Name of the environment variable. Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". + config: + properties: + annotations: + additionalProperties: type: string - valueFrom: - description: Source for the environment variable's value. Cannot - be used if value is not empty. + type: object + appPorts: + default: + - name: devbox-app-port + port: 8080 + protocol: TCP + items: + description: ServicePort contains information on service's port. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is - written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified - API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + appProtocol: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed - resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret or its key must - be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - extraLabels: - additionalProperties: - type: string - description: todo add rewrite labels and annotations... - type: object - extraVolumeMounts: - items: - description: VolumeMount describes a mounting of a Volume within - a container. - properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - When not set, MountPropagationNone is used. - This field is beta in 1.10. - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - subPath: - description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: |- - Expanded path within the volume from which the container's volume should be mounted. - Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. - Defaults to "" (volume's root). - SubPathExpr and SubPath are mutually exclusive. - type: string - required: - - mountPath - - name - type: object - type: array - extraVolumes: - description: todo add rewrite volumes and volume mounts.. - items: - description: Volume represents a named volume in a pod that may - be accessed by any container in the pod. - properties: - awsElasticBlockStore: - description: |- - awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - properties: - fsType: + The application protocol for this port. + This is used as a hint for implementations to offer richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: + + + * Un-prefixed protocol names - reserved for IANA standard service names (as per + RFC-6335 and https://www.iana.org/assignments/service-names). + + + * Kubernetes-defined prefixed names: + * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 + * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 + + + * Other protocols should use implementation-defined prefixed names such as + mycompany.com/my-custom-protocol. + type: string + name: description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - TODO: how do we prevent errors in the filesystem from compromising the machine + The name of this port within the service. This must be a DNS_LABEL. + All ports within a ServiceSpec must have unique names. When considering + the endpoints for a Service, this must match the 'name' field in the + EndpointPort. + Optional if only one ServicePort is defined on this service. type: string - partition: + nodePort: description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - Examples: For volume /dev/sda1, you specify the partition as "1". - Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + The port on each node on which this service is exposed when type is + NodePort or LoadBalancer. Usually assigned by the system. If a value is + specified, in-range, and not in use it will be used, otherwise the + operation will fail. If not specified, a port will be allocated if this + Service requires one. If this field is specified when creating a + Service which does not need it, creation will fail. This field will be + wiped when updating a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). + More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport format: int32 type: integer - readOnly: - description: |- - readOnly value true will force the readOnly setting in VolumeMounts. - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - type: boolean - volumeID: - description: |- - volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). - More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - type: string - required: - - volumeID - type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount on - the host and bind mount to the pod. - properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: None, - Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in the - blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the blob - storage - type: string - fsType: + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP description: |- - fsType is Filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - kind: - description: 'kind expected values are Shared: multiple - blob disks per storage account Dedicated: single blob - disk per storage account Managed: azure managed data - disk (only in managed availability set). defaults to shared' + The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". + Default is TCP. type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service mount - on the host and bind mount to the pod. - properties: - readOnly: + targetPort: + anyOf: + - type: integer + - type: string description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that contains - Azure Storage Account Name and Key - type: string - shareName: - description: shareName is the azure share Name - type: string + Number or name of the port to access on the pods targeted by the service. + Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named port in the + target Pod's container ports. If this is not specified, the value + of the 'port' field is used (an identity map). + This field is ignored for services with clusterIP=None, and should be + omitted or set equal to the 'port' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + x-kubernetes-int-or-string: true required: - - secretName - - shareName + - port type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host that - shares a pod's lifetime + type: array + args: + description: kubebuilder:validation:Optional + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. properties: - monitors: - description: |- - monitors is Required: Monitors is a collection of Ceph monitors - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - items: - type: string - type: array - path: - description: 'path is Optional: Used as the mounted root, - rather than the full Ceph tree, default is /' + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. type: string - readOnly: - description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: boolean - secretFile: + value: description: |- - secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - secretRef: - description: |- - secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is optional: User is the rados user name, default is admin - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it - type: string - required: - - monitors - type: object - cinder: - description: |- - cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: boolean - secretRef: - description: |- - secretRef is optional: points to a secret object containing parameters used to connect - to OpenStack. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - volumeID: - description: |- - volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: string - required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should populate - this volume - properties: - defaultMode: - description: |- - defaultMode is optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the ConfigMap, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional specify whether the ConfigMap or its - keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - csi: - description: csi (Container Storage Interface) represents ephemeral - storage that is handled by certain external CSI drivers (Beta - feature). - properties: - driver: - description: |- - driver is the name of the CSI driver that handles this volume. - Consult with your admin for the correct name as registered in the cluster. - type: string - fsType: - description: |- - fsType to mount. Ex. "ext4", "xfs", "ntfs". - If not provided, the empty value is passed to the associated CSI driver - which will determine the default filesystem to apply. - type: string - nodePublishSecretRef: - description: |- - nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to complete the CSI - NodePublishVolume and NodeUnpublishVolume calls. - This field is optional, and may be empty if no secret is required. If the - secret object contains more than one secret, all secret references are passed. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - readOnly: - description: |- - readOnly specifies a read-only configuration for the volume. - Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: |- - volumeAttributes stores driver-specific properties that are passed to the CSI - driver. Consult your driver's documentation for supported values. - type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the pod - that should populate this volume - properties: - defaultMode: - description: |- - Optional: mode bits to use on created files by default. Must be a - Optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: Items is a list of downward API volume file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field - properties: - fieldRef: - description: 'Required: Selects a field of the pod: - only annotations, labels, name and namespace are - supported.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - mode: - description: |- - Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: 'Required: Path is the relative path - name of the file to be created. Must not be absolute - or contain the ''..'' path. Must be utf-8 encoded. - The first item of the relative path must not start - with ''..''' - type: string - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - required: - - path - type: object - type: array - type: object - emptyDir: - description: |- - emptyDir represents a temporary directory that shares a pod's lifetime. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - properties: - medium: - description: |- - medium represents what type of storage medium should back this directory. - The default is "" which means to use the node's default medium. - Must be an empty string (default) or Memory. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - type: string - sizeLimit: - anyOf: - - type: integer - - type: string - description: |- - sizeLimit is the total amount of local storage required for this EmptyDir volume. - The size limit is also applicable for memory medium. - The maximum usage on memory medium EmptyDir would be the minimum value between - the SizeLimit specified here and the sum of memory limits of all containers in a pod. - The default is nil which means that the limit is undefined. - More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - ephemeral: - description: |- - ephemeral represents a volume that is handled by a cluster storage driver. - The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, - and deleted when the pod is removed. - - - Use this if: - a) the volume is only needed while the pod runs, - b) features of normal volumes like restoring from snapshot or capacity - tracking are needed, - c) the storage driver is specified through a storage class, and - d) the storage driver supports dynamic volume provisioning through - a PersistentVolumeClaim (see EphemeralVolumeSource for more - information on the connection between this volume type - and PersistentVolumeClaim). - - - Use PersistentVolumeClaim or one of the vendor-specific - APIs for volumes that persist for longer than the lifecycle - of an individual pod. - - - Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to - be used that way - see the documentation of the driver for - more information. - - - A pod can use both types of ephemeral volumes and - persistent volumes at the same time. - properties: - volumeClaimTemplate: - description: |- - Will be used to create a stand-alone PVC to provision the volume. - The pod in which this EphemeralVolumeSource is embedded will be the - owner of the PVC, i.e. the PVC will be deleted together with the - pod. The name of the PVC will be `-` where - `` is the name from the `PodSpec.Volumes` array - entry. Pod validation will reject the pod if the concatenated name - is not valid for a PVC (for example, too long). - - - An existing PVC with that name that is not owned by the pod - will *not* be used for the pod to avoid using an unrelated - volume by mistake. Starting the pod is then blocked until - the unrelated PVC is removed. If such a pre-created PVC is - meant to be used by the pod, the PVC has to updated with an - owner reference to the pod once the pod exists. Normally - this should not be necessary, but it may be useful when - manually reconstructing a broken cluster. - - - This field is read-only and no changes will be made by Kubernetes - to the PVC after it has been created. - - - Required, must not be nil. - properties: - metadata: - description: |- - May contain labels and annotations that will be copied into the PVC - when creating it. No other fields are allowed and will be rejected during - validation. - type: object - spec: - description: |- - The specification for the PersistentVolumeClaim. The entire content is - copied unchanged into the PVC that gets created from this - template. The same fields as in a PersistentVolumeClaim - are also valid here. - properties: - accessModes: - description: |- - accessModes contains the desired access modes the volume should have. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 - items: - type: string - type: array - dataSource: - description: |- - dataSource field can be used to specify either: - * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) - If the provisioner or an external controller can support the specified data source, - it will create a new volume based on the contents of the specified data source. - When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, - and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. - If the namespace is specified, then dataSourceRef will not be copied to dataSource. - properties: - apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - required: - - kind - - name - type: object - x-kubernetes-map-type: atomic - dataSourceRef: - description: |- - dataSourceRef specifies the object from which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a non-empty API group (non - core object) or a PersistentVolumeClaim object. - When this field is specified, volume binding will only succeed if the type of - the specified object matches some installed volume populator or dynamic - provisioner. - This field will replace the functionality of the dataSource field and as such - if both fields are non-empty, they must have the same value. For backwards - compatibility, when namespace isn't specified in dataSourceRef, - both fields (dataSource and dataSourceRef) will be set to the same - value automatically if one of them is empty and the other is non-empty. - When namespace is specified in dataSourceRef, - dataSource isn't set to the same value and must be empty. - There are three important differences between dataSource and dataSourceRef: - * While dataSource only allows two specific types of objects, dataSourceRef - allows any non-core object, as well as PersistentVolumeClaim objects. - * While dataSource ignores disallowed values (dropping them), dataSourceRef - preserves all values, and generates an error if a disallowed value is - specified. - * While dataSource only allows local objects, dataSourceRef allows objects - in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. - (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. - properties: - apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - namespace: - description: |- - Namespace is the namespace of resource being referenced - Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. - type: string - required: - - kind - - name - type: object - resources: - description: |- - resources represents the minimum resources the volume should have. - If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements - that are lower than previous value but must still be higher than capacity recorded in the - status field of the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one - entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - selector: - description: selector is a label query over volumes - to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - storageClassName: - description: |- - storageClassName is the name of the StorageClass required by the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 - type: string - volumeMode: - description: |- - volumeMode defines what type of volume is required by the claim. - Value of Filesystem is implied when not included in claim spec. - type: string - volumeName: - description: volumeName is the binding reference - to the PersistentVolume backing this claim. - type: string - type: object - required: - - spec - type: object - type: object - fc: - description: fc represents a Fibre Channel resource that is - attached to a kubelet's host machine and then exposed to the - pod. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - wwids: - description: |- - wwids Optional: FC volume world wide identifiers (wwids) - Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. - items: - type: string - type: array - type: object - flexVolume: - description: |- - flexVolume represents a generic volume resource that is - provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use for - this volume. - type: string - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds extra - command options if any.' - type: object - readOnly: - description: |- - readOnly is Optional: defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugin scripts. This may be - empty if no secret object is specified. If the secret object - contains more than one secret, all secrets are passed to the plugin - scripts. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached to - a kubelet's host machine. This depends on the Flocker control - service being running - properties: - datasetName: - description: |- - datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as deprecated - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. This - is unique identifier of a Flocker dataset - type: string - type: object - gcePersistentDisk: - description: |- - gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - properties: - fsType: - description: |- - fsType is filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - partition: - description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - Examples: For volume /dev/sda1, you specify the partition as "1". - Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - format: int32 - type: integer - pdName: - description: |- - pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - type: boolean - required: - - pdName - type: object - gitRepo: - description: |- - gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an - EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir - into the Pod's container. - properties: - directory: - description: |- - directory is the target directory name. - Must not contain or start with '..'. If '.' is supplied, the volume directory will be the - git repository. Otherwise, if specified, the volume will contain the git repository in - the subdirectory with the given name. - type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s.io/volumes/glusterfs/README.md - properties: - endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: string - path: - description: |- - path is the Glusterfs volume path. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: string - readOnly: - description: |- - readOnly here will force the Glusterfs volume to be mounted with read-only permissions. - Defaults to false. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: boolean - required: - - endpoints - - path - type: object - hostPath: - description: |- - hostPath represents a pre-existing file or directory on the host - machine that is directly exposed to the container. This is generally - used for system agents or other privileged things that are allowed - to see the host machine. Most containers will NOT need this. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - --- - TODO(jonesdl) We need to restrict who can use host directory mounts and who can/can not - mount host directories as read/write. - properties: - path: - description: |- - path of the directory on the host. - If the path is a symlink, it will follow the link to the real path. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - type: string - type: - description: |- - type for HostPath Volume - Defaults to "" - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - type: string - required: - - path - type: object - iscsi: - description: |- - iscsi represents an ISCSI Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - More info: https://examples.k8s.io/volumes/iscsi/README.md - properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support iSCSI - Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support iSCSI - Session CHAP authentication - type: boolean - fsType: - description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - initiatorName: - description: |- - initiatorName is the custom iSCSI Initiator Name. - If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface - : will be created for the connection. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. - type: string - iscsiInterface: - description: |- - iscsiInterface is the interface Name that uses an iSCSI transport. - Defaults to 'default' (tcp). - type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: |- - portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port - is other than default (typically TCP ports 860 and 3260). - items: - type: string - type: array - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI target - and initiator authentication - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - targetPortal: - description: |- - targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port - is other than default (typically TCP ports 860 and 3260). - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: |- - name of the volume. - Must be a DNS_LABEL and unique within the pod. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - nfs: - description: |- - nfs represents an NFS mount on the host that shares a pod's lifetime - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - properties: - path: - description: |- - path that is exported by the NFS server. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: string - readOnly: - description: |- - readOnly here will force the NFS export to be mounted with read-only permissions. - Defaults to false. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: boolean - server: - description: |- - server is the hostname or IP address of the NFS server. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: |- - persistentVolumeClaimVolumeSource represents a reference to a - PersistentVolumeClaim in the same namespace. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims - properties: - claimName: - description: |- - claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims - type: string - readOnly: - description: |- - readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean - required: - - claimName - type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID - type: object - portworxVolume: - description: portworxVolume represents a portworx volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx volume - type: string - required: - - volumeID - type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API - properties: - defaultMode: - description: |- - defaultMode are the mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along with - other supported volume types - properties: - configMap: - description: configMap information about the configMap - data to project - properties: - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the ConfigMap, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - downwardAPI: - description: downwardAPI information about the downwardAPI - data to project - properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing - the pod field - properties: - fieldRef: - description: 'Required: Selects a field - of the pod: only annotations, labels, - name and namespace are supported.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - mode: - description: |- - Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must - not be absolute or contain the ''..'' - path. Must be utf-8 encoded. The first - item of the relative path must not start - with ''..''' - type: string - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - required: - - path - type: object - type: array - type: object - secret: - description: secret information about the secret data - to project - properties: - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the Secret, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: optional field specify whether the - Secret or its key must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - serviceAccountToken: - description: serviceAccountToken is information about - the serviceAccountToken data to project - properties: - audience: - description: |- - audience is the intended audience of the token. A recipient of a token - must identify itself with an identifier specified in the audience of the - token, and otherwise should reject the token. The audience defaults to the - identifier of the apiserver. - type: string - expirationSeconds: - description: |- - expirationSeconds is the requested duration of validity of the service - account token. As the token approaches expiration, the kubelet volume - plugin will proactively rotate the service account token. The kubelet will - start trying to rotate the token if the token is older than 80 percent of - its time to live or if the token is older than 24 hours.Defaults to 1 hour - and must be at least 10 minutes. - format: int64 - type: integer - path: - description: |- - path is the path relative to the mount point of the file to project the - token into. - type: string - required: - - path - type: object - type: object - type: array - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime - properties: - group: - description: |- - group to map volume access to - Default is no group - type: string - readOnly: - description: |- - readOnly here will force the Quobyte volume to be mounted with read-only permissions. - Defaults to false. - type: boolean - registry: - description: |- - registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple entries are separated with commas) - which acts as the central registry for volumes - type: string - tenant: - description: |- - tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by the plugin - type: string - user: - description: |- - user to map volume access to - Defaults to serivceaccount user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s.io/volumes/rbd/README.md - properties: - fsType: - description: |- - fsType is the filesystem type of the volume that you want to mount. - Tip: Ensure that the filesystem type is supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd - TODO: how do we prevent errors in the filesystem from compromising the machine - type: string - image: - description: |- - image is the rados image name. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - keyring: - description: |- - keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - monitors: - description: |- - monitors is a collection of Ceph monitors. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - items: - type: string - type: array - pool: - description: |- - pool is the rados pool name. - Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: boolean - secretRef: - description: |- - secretRef is name of the authentication secret for RBDUser. If provided - overrides keyring. - Default is nil. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is the rados user name. - Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume - attached and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". - Default is "xfs". - type: string - gateway: - description: gateway is the host address of the ScaleIO - API Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO - Protection Domain for the configured storage. - type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef references to the secret for ScaleIO user and other - sensitive information. If this is not provided, Login operation will fail. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: |- - storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. - Default is ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool associated - with the protection domain. - type: string - system: - description: system is the name of the storage system as - configured in ScaleIO. - type: string - volumeName: - description: |- - volumeName is the name of a volume already created in the ScaleIO system - that is associated with this volume source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: |- - secret represents a secret that should populate this volume. - More info: https://kubernetes.io/docs/concepts/storage/volumes#secret - properties: - defaultMode: - description: |- - defaultMode is Optional: mode bits used to set permissions on created files by default. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values - for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - items: - description: |- - items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume as a file whose name is the - key and content is the value. If specified, the listed keys will be - projected into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the Secret, - the volume setup will error unless it is marked optional. Paths must be - relative and may not contain the '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: |- - mode is Optional: mode bits used to set permissions on this file. - Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - If not specified, the volume defaultMode will be used. - This might be in conflict with other options that affect the file - mode, like fsGroup, and the result can be other mode bits set. - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - May not contain the path element '..'. - May not start with the string '..'. - type: string - required: - - key - - path - type: object - type: array - optional: - description: optional field specify whether the Secret or - its keys must be defined - type: boolean - secretName: - description: |- - secretName is the name of the secret in the pod's namespace to use. - More info: https://kubernetes.io/docs/concepts/storage/volumes#secret - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef specifies the secret to use for obtaining the StorageOS API - credentials. If not specified, default values will be attempted. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - type: object - x-kubernetes-map-type: atomic - volumeName: - description: |- - volumeName is the human-readable name of the StorageOS volume. Volume - names are only unique within a namespace. - type: string - volumeNamespace: - description: |- - volumeNamespace specifies the scope of the volume within StorageOS. If no - namespace is specified then the Pod's namespace will be used. This allows the - Kubernetes name scoping to be mirrored within StorageOS for tighter integration. - Set VolumeName to any name to override the default behaviour. - Set to "default" if you are not using namespaces within StorageOS. - Namespaces that do not pre-exist within StorageOS will be created. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based - Management (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy Based - Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies vSphere - volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - type: array - network: - properties: - extraPorts: - items: - description: ContainerPort represents a network port in a single - container. - properties: - containerPort: - description: |- - Number of port to expose on the pod's IP address. - This must be a valid port number, 0 < x < 65536. - format: int32 - type: integer - hostIP: - description: What host IP to bind the external port to. - type: string - hostPort: - description: |- - Number of port to expose on the host. - If specified, this must be a valid port number, 0 < x < 65536. - If HostNetwork is specified, this must match ContainerPort. - Most containers do not need this. - format: int32 - type: integer - name: - description: |- - If specified, this must be an IANA_SVC_NAME and unique within the pod. Each - named port in a pod must have a unique name. Name for the port that can be - referred to by services. - type: string - protocol: - default: TCP - description: |- - Protocol for port. Must be UDP, TCP, or SCTP. - Defaults to "TCP". - type: string - required: - - containerPort - type: object - type: array - type: - enum: - - NodePort - - Tailnet - type: string - required: - - type - type: object - resource: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - runtimeRef: - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object - squash: - default: false - type: boolean - state: - enum: - - Running - - Stopped - type: string - tolerations: - items: - description: |- - The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . - properties: - effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. - type: string - operator: - description: |- - Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. - type: string - tolerationSeconds: - description: |- - TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). Zero and - negative values will be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. - type: string - type: object - type: array - workingDir: - type: string - required: - - resource - - runtimeRef - - state - type: object - status: - description: DevboxStatus defines the observed state of Devbox - properties: - commitHistory: - items: - properties: - containerID: - description: ContainerID is the container id - type: string - image: - description: Image is the image of the commit - type: string - node: - description: Node is the node name - type: string - pod: - description: Pod is the pod name - type: string - predicatedStatus: - description: predicatedStatus default `pending`, will be set - to `success` if pod status is running successfully. - type: string - status: - description: status will be set based on expectedStatus after - devbox pod delete or stop. if expectedStatus is still pending, - it means the pod is not running successfully, so we need to - set it to `failed` - type: string - time: - description: Time is the time when the commit is created - format: date-time - type: string - required: - - containerID - - image - - node - - pod - - predicatedStatus - - status - - time - type: object - type: array - lastState: - description: |- - ContainerState holds a possible state of container. - Only one of its members may be specified. - If none of them is specified, the default one is ContainerStateWaiting. - properties: - running: - description: Details about a running container - properties: - startedAt: - description: Time at which the container was last (re-)started - format: date-time - type: string - type: object - terminated: - description: Details about a terminated container - properties: - containerID: - description: Container's ID in the format '://' - type: string - exitCode: - description: Exit status from the last termination of the - container - format: int32 - type: integer - finishedAt: - description: Time at which the container last terminated - format: date-time - type: string - message: - description: Message regarding the last termination of the - container - type: string - reason: - description: (brief) reason from the last termination of the - container - type: string - signal: - description: Signal from the last termination of the container - format: int32 - type: integer - startedAt: - description: Time at which previous execution of the container - started - format: date-time - type: string - required: - - exitCode - type: object - waiting: - description: Details about a waiting container - properties: - message: - description: Message regarding why the container is not yet - running. - type: string - reason: - description: (brief) reason the container is not yet running. - type: string - type: object - type: object - network: - properties: - nodePort: - format: int32 - type: integer - tailnet: - description: todo TailNet - type: string - type: - default: NodePort - enum: - - NodePort - - Tailnet - type: string - required: - - type - type: object - phase: - type: string - state: - description: |- - ContainerState holds a possible state of container. - Only one of its members may be specified. - If none of them is specified, the default one is ContainerStateWaiting. - properties: - running: - description: Details about a running container - properties: - startedAt: - description: Time at which the container was last (re-)started - format: date-time - type: string - type: object - terminated: - description: Details about a terminated container - properties: - containerID: - description: Container's ID in the format '://' - type: string - exitCode: - description: Exit status from the last termination of the - container - format: int32 - type: integer - finishedAt: - description: Time at which the container last terminated - format: date-time - type: string - message: - description: Message regarding the last termination of the - container - type: string - reason: - description: (brief) reason from the last termination of the - container - type: string - signal: - description: Signal from the last termination of the container - format: int32 - type: integer - startedAt: - description: Time at which previous execution of the container - started - format: date-time - type: string - required: - - exitCode - type: object - waiting: - description: Details about a waiting container - properties: - message: - description: Message regarding why the container is not yet - running. - type: string - reason: - description: (brief) reason the container is not yet running. - type: string - type: object - type: object - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: devboxreleases.devbox.sealos.io -spec: - group: devbox.sealos.io - names: - kind: DevBoxRelease - listKind: DevBoxReleaseList - plural: devboxreleases - singular: devboxrelease - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.devboxName - name: DevboxName - type: string - - jsonPath: .spec.newTag - name: NewTag - type: string - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .status.originalImage - name: OriginalImage - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: DevBoxRelease is the Schema for the devboxreleases API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: DevBoxReleaseSpec defines the desired state of DevBoxRelease - properties: - devboxName: - type: string - newTag: - type: string - notes: - type: string - required: - - devboxName - - newTag - type: object - status: - description: DevBoxReleaseStatus defines the observed state of DevBoxRelease - properties: - originalImage: - type: string - phase: - default: Pending - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: operationrequests.devbox.sealos.io -spec: - group: devbox.sealos.io - names: - kind: OperationRequest - listKind: OperationRequestList - plural: operationrequests - singular: operationrequest - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: OperationRequest is the Schema for the operationrequests API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: OperationRequestSpec defines the desired state of OperationRequest - type: object - status: - description: OperationRequestStatus defines the observed state of OperationRequest - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: runtimeclasses.devbox.sealos.io -spec: - group: devbox.sealos.io - names: - kind: RuntimeClass - listKind: RuntimeClassList - plural: runtimeclasses - singular: runtimeclass - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: RuntimeClass is the Schema for the runtimeclasses API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: RuntimeClassSpec defines the desired state of RuntimeClass - properties: - description: - type: string - kind: - enum: - - OS - - Language - - Framework - type: string - title: - type: string - required: - - kind - - title - type: object - status: - description: RuntimeClassStatus defines the observed state of RuntimeClass - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: runtimes.devbox.sealos.io -spec: - group: devbox.sealos.io - names: - kind: Runtime - listKind: RuntimeList - plural: runtimes - singular: runtime - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.classRef - name: Class - type: string - - jsonPath: .spec.version - name: Version - type: string - - jsonPath: .spec.runtimeVersion - name: RuntimeVersion - type: string - - jsonPath: .spec.state - name: State - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: Runtime is the Schema for the runtimes API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: RuntimeSpec defines the desired state of Runtime - properties: - category: - items: - type: string - type: array - classRef: - type: string - components: - items: - properties: - name: - type: string - version: - type: string - required: - - name - - version - type: object - type: array - config: - properties: - annotations: - additionalProperties: - type: string - type: object - appPorts: - default: - - name: devbox-app-port - port: 8080 - protocol: TCP - items: - description: ServicePort contains information on service's port. - properties: - appProtocol: - description: |- - The application protocol for this port. - This is used as a hint for implementations to offer richer behavior for protocols that they understand. - This field follows standard Kubernetes label syntax. - Valid values are either: - - - * Un-prefixed protocol names - reserved for IANA standard service names (as per - RFC-6335 and https://www.iana.org/assignments/service-names). - - - * Kubernetes-defined prefixed names: - * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 - * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 - * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 - - - * Other protocols should use implementation-defined prefixed names such as - mycompany.com/my-custom-protocol. - type: string - name: - description: |- - The name of this port within the service. This must be a DNS_LABEL. - All ports within a ServiceSpec must have unique names. When considering - the endpoints for a Service, this must match the 'name' field in the - EndpointPort. - Optional if only one ServicePort is defined on this service. - type: string - nodePort: - description: |- - The port on each node on which this service is exposed when type is - NodePort or LoadBalancer. Usually assigned by the system. If a value is - specified, in-range, and not in use it will be used, otherwise the - operation will fail. If not specified, a port will be allocated if this - Service requires one. If this field is specified when creating a - Service which does not need it, creation will fail. This field will be - wiped when updating a Service to no longer need it (e.g. changing type - from NodePort to ClusterIP). - More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport - format: int32 - type: integer - port: - description: The port that will be exposed by this service. - format: int32 - type: integer - protocol: - default: TCP - description: |- - The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". - Default is TCP. - type: string - targetPort: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the pods targeted by the service. - Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. - If this is a string, it will be looked up as a named port in the - target Pod's container ports. If this is not specified, the value - of the 'port' field is used (an identity map). - This field is ignored for services with clusterIP=None, and should be - omitted or set equal to the 'port' field. - More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service - x-kubernetes-int-or-string: true - required: - - port - type: object - type: array - args: - description: kubebuilder:validation:Optional - items: - type: string - type: array - command: - items: - type: string - type: array - env: - items: - description: EnvVar represents an environment variable present - in a Container. - properties: - name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. - Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: description: |- Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. @@ -3391,8 +1042,6 @@ spec: - name type: object type: array - image: - type: string labels: additionalProperties: type: string @@ -3443,7 +1092,7 @@ spec: type: array releaseArgs: default: - - /home/sealos/project/entrypoint.sh + - /home/devbox/project/entrypoint.sh items: type: string type: array @@ -3455,7 +1104,7 @@ spec: type: string type: array user: - default: sealos + default: devbox type: string volumeMounts: items: @@ -5109,30 +2758,393 @@ spec: type: object type: array workingDir: - default: /home/sealos/project + default: /home/devbox/project + type: string + type: object + image: + type: string + network: + properties: + type: + enum: + - NodePort + - Tailnet + type: string + required: + - type + type: object + resource: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + squash: + default: false + type: boolean + state: + enum: + - Running + - Stopped + type: string + templateID: + type: string + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + required: + - config + - image + - resource + - state + type: object + status: + description: DevboxStatus defines the observed state of Devbox + properties: + commitHistory: + items: + properties: + containerID: + description: ContainerID is the container id + type: string + image: + description: Image is the image of the commit + type: string + node: + description: Node is the node name + type: string + pod: + description: Pod is the pod name + type: string + predicatedStatus: + description: predicatedStatus default `pending`, will be set + to `success` if pod status is running successfully. + type: string + status: + description: status will be set based on expectedStatus after + devbox pod delete or stop. if expectedStatus is still pending, + it means the pod is not running successfully, so we need to + set it to `failed` + type: string + time: + description: Time is the time when the commit is created + format: date-time + type: string + required: + - containerID + - image + - node + - pod + - predicatedStatus + - status + - time + type: object + type: array + lastState: + description: |- + ContainerState holds a possible state of container. + Only one of its members may be specified. + If none of them is specified, the default one is ContainerStateWaiting. + properties: + running: + description: Details about a running container + properties: + startedAt: + description: Time at which the container was last (re-)started + format: date-time + type: string + type: object + terminated: + description: Details about a terminated container + properties: + containerID: + description: Container's ID in the format '://' + type: string + exitCode: + description: Exit status from the last termination of the + container + format: int32 + type: integer + finishedAt: + description: Time at which the container last terminated + format: date-time + type: string + message: + description: Message regarding the last termination of the + container + type: string + reason: + description: (brief) reason from the last termination of the + container + type: string + signal: + description: Signal from the last termination of the container + format: int32 + type: integer + startedAt: + description: Time at which previous execution of the container + started + format: date-time + type: string + required: + - exitCode + type: object + waiting: + description: Details about a waiting container + properties: + message: + description: Message regarding why the container is not yet + running. + type: string + reason: + description: (brief) reason the container is not yet running. + type: string + type: object + type: object + network: + properties: + nodePort: + format: int32 + type: integer + tailnet: + description: todo TailNet + type: string + type: + default: NodePort + enum: + - NodePort + - Tailnet type: string required: - - image + - type + type: object + phase: + type: string + state: + description: |- + ContainerState holds a possible state of container. + Only one of its members may be specified. + If none of them is specified, the default one is ContainerStateWaiting. + properties: + running: + description: Details about a running container + properties: + startedAt: + description: Time at which the container was last (re-)started + format: date-time + type: string + type: object + terminated: + description: Details about a terminated container + properties: + containerID: + description: Container's ID in the format '://' + type: string + exitCode: + description: Exit status from the last termination of the + container + format: int32 + type: integer + finishedAt: + description: Time at which the container last terminated + format: date-time + type: string + message: + description: Message regarding the last termination of the + container + type: string + reason: + description: (brief) reason from the last termination of the + container + type: string + signal: + description: Signal from the last termination of the container + format: int32 + type: integer + startedAt: + description: Time at which previous execution of the container + started + format: date-time + type: string + required: + - exitCode + type: object + waiting: + description: Details about a waiting container + properties: + message: + description: Message regarding why the container is not yet + running. + type: string + reason: + description: (brief) reason the container is not yet running. + type: string + type: object type: object - description: - type: string - runtimeVersion: + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: devboxreleases.devbox.sealos.io +spec: + group: devbox.sealos.io + names: + kind: DevBoxRelease + listKind: DevBoxReleaseList + plural: devboxreleases + singular: devboxrelease + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.devboxName + name: DevboxName + type: string + - jsonPath: .spec.newTag + name: NewTag + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.originalImage + name: OriginalImage + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: DevBoxRelease is the Schema for the devboxreleases API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DevBoxReleaseSpec defines the desired state of DevBoxRelease + properties: + devboxName: type: string - state: - default: active - enum: - - active - - deprecated + newTag: type: string - version: + notes: type: string required: - - classRef - - config - - version + - devboxName + - newTag + type: object + status: + description: DevBoxReleaseStatus defines the observed state of DevBoxRelease + properties: + originalImage: + type: string + phase: + default: Pending + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: operationrequests.devbox.sealos.io +spec: + group: devbox.sealos.io + names: + kind: OperationRequest + listKind: OperationRequestList + plural: operationrequests + singular: operationrequest + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: OperationRequest is the Schema for the operationrequests API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OperationRequestSpec defines the desired state of OperationRequest type: object status: - description: RuntimeStatus defines the observed state of Runtime + description: OperationRequestStatus defines the observed state of OperationRequest type: object type: object served: true @@ -5483,106 +3495,6 @@ rules: - get --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: devbox - name: devbox-runtime-editor-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimes - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimes/status - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: devbox - name: devbox-runtime-viewer-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimes - verbs: - - get - - list - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimes/status - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: devbox - name: devbox-runtimeclass-editor-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses/status - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: devbox - name: devbox-runtimeclass-viewer-role -rules: -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses - verbs: - - get - - list - - watch -- apiGroups: - - devbox.sealos.io - resources: - - runtimeclasses/status - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: @@ -5675,7 +3587,6 @@ spec: - --registry-addr={{ .registryAddr }} - --registry-user={{ .registryUser }} - --registry-password={{ .registryPassword }} - - --auth-addr={{ .authAddr }} command: - /manager image: ghcr.io/labring/sealos-devbox-controller:latest diff --git a/controllers/devbox/internal/controller/devbox_controller.go b/controllers/devbox/internal/controller/devbox_controller.go index 76fcb82d9dae..54eb2e84c015 100644 --- a/controllers/devbox/internal/controller/devbox_controller.go +++ b/controllers/devbox/internal/controller/devbox_controller.go @@ -23,6 +23,8 @@ import ( devboxv1alpha1 "github.com/labring/sealos/controllers/devbox/api/v1alpha1" "github.com/labring/sealos/controllers/devbox/internal/controller/helper" + "github.com/labring/sealos/controllers/devbox/internal/controller/utils/matcher" + "github.com/labring/sealos/controllers/devbox/internal/controller/utils/resource" "github.com/labring/sealos/controllers/devbox/label" corev1 "k8s.io/api/core/v1" @@ -37,6 +39,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -44,11 +47,12 @@ import ( // DevboxReconciler reconciles a Devbox object type DevboxReconciler struct { - CommitImageRegistry string - RequestCPURate float64 - RequestMemoryRate float64 - RequestEphemeralStorage string - LimitEphemeralStorage string + CommitImageRegistry string + + RequestRate resource.RequestRate + EphemeralStorage resource.EphemeralStorage + + PodMatchers []matcher.PodMatcher DebugMode bool @@ -260,12 +264,8 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D switch devbox.Spec.State { case devboxv1alpha1.DevboxStateRunning: - runtimecr, err := r.getRuntime(ctx, devbox) - if err != nil { - return err - } nextCommitHistory := r.generateNextCommitHistory(devbox) - expectPod := r.generateDevboxPod(devbox, runtimecr, nextCommitHistory) + expectPod := r.generateDevboxPod(devbox, nextCommitHistory) switch len(podList.Items) { case 0: @@ -298,7 +298,7 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D logger.Info("pod has been deleted") return r.handlePodDeleted(ctx, devbox, pod) } - switch helper.PodMatchExpectations(expectPod, pod) { + switch matcher.PodMatchExpectations(expectPod, pod, r.PodMatchers...) { case true: // pod match expectations logger.Info("pod match expectations") @@ -343,12 +343,8 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D } func (r *DevboxReconciler) syncService(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error { - runtimecr, err := r.getRuntime(ctx, devbox) - if err != nil { - return err - } var servicePorts []corev1.ServicePort - for _, port := range runtimecr.Spec.Config.Ports { + for _, port := range devbox.Spec.Config.Ports { servicePorts = append(servicePorts, corev1.ServicePort{ Name: port.Name, Port: port.ContainerPort, @@ -399,7 +395,7 @@ func (r *DevboxReconciler) syncService(ctx context.Context, devbox *devboxv1alph // Retrieve the updated Service to get the NodePort var updatedService corev1.Service - err = retry.OnError( + err := retry.OnError( retry.DefaultRetry, func(err error) bool { return client.IgnoreNotFound(err) == nil }, func() error { @@ -426,26 +422,27 @@ func (r *DevboxReconciler) syncService(ctx context.Context, devbox *devboxv1alph return r.Status().Update(ctx, devbox) } -// get the runtime -func (r *DevboxReconciler) getRuntime(ctx context.Context, devbox *devboxv1alpha1.Devbox) (*devboxv1alpha1.Runtime, error) { - runtimeNamespace := devbox.Spec.RuntimeRef.Namespace - if runtimeNamespace == "" { - runtimeNamespace = devbox.Namespace - } - runtimecr := &devboxv1alpha1.Runtime{} - if err := r.Get(ctx, client.ObjectKey{Namespace: runtimeNamespace, Name: devbox.Spec.RuntimeRef.Name}, runtimecr); err != nil { - return nil, err - } - return runtimecr, nil -} - // create a new pod, add predicated status to nextCommitHistory func (r *DevboxReconciler) createPod(ctx context.Context, devbox *devboxv1alpha1.Devbox, expectPod *corev1.Pod, nextCommitHistory *devboxv1alpha1.CommitHistory) error { + logger := log.FromContext(ctx) + + logger.Info("creating pod", + "podName", expectPod.Name, + "namespace", expectPod.Namespace, + "nextCommitHistory", nextCommitHistory) + nextCommitHistory.Status = devboxv1alpha1.CommitStatusPending nextCommitHistory.PredicatedStatus = devboxv1alpha1.CommitStatusPending + + if expectPod.Name == "" { + return fmt.Errorf("pod name cannot be empty") + } + if err := r.Create(ctx, expectPod); err != nil { + logger.Error(err, "failed to create pod") return err } + devbox.Status.CommitHistory = append(devbox.Status.CommitHistory, nextCommitHistory) return nil } @@ -463,7 +460,9 @@ func (r *DevboxReconciler) deletePod(ctx context.Context, devbox *devboxv1alpha1 return err } // update commit history status because pod has been deleted - devbox.Status.LastTerminationState = pod.Status.ContainerStatuses[0].State + if len(pod.Status.ContainerStatuses) != 0 { + devbox.Status.LastTerminationState = pod.Status.ContainerStatuses[0].State + } helper.UpdateCommitHistory(devbox, pod, true) return nil } @@ -476,8 +475,10 @@ func (r *DevboxReconciler) handlePodDeleted(ctx context.Context, devbox *devboxv return err } // update commit history status because pod has been deleted + if len(pod.Status.ContainerStatuses) != 0 { + devbox.Status.LastTerminationState = pod.Status.ContainerStatuses[0].State + } helper.UpdateCommitHistory(devbox, pod, true) - devbox.Status.LastTerminationState = pod.Status.ContainerStatuses[0].State return nil } @@ -513,38 +514,34 @@ func (r *DevboxReconciler) deleteResourcesByLabels(ctx context.Context, obj clie return client.IgnoreNotFound(err) } -func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Runtime, nextCommitHistory *devboxv1alpha1.CommitHistory) *corev1.Pod { +func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, nextCommitHistory *devboxv1alpha1.CommitHistory) *corev1.Pod { objectMeta := metav1.ObjectMeta{ Name: nextCommitHistory.Pod, Namespace: devbox.Namespace, - Labels: helper.GeneratePodLabels(devbox, runtime), - Annotations: helper.GeneratePodAnnotations(devbox, runtime), + Labels: helper.GeneratePodLabels(devbox), + Annotations: helper.GeneratePodAnnotations(devbox), } - // set up ports and env by using runtime ports and devbox extra ports - ports := runtime.Spec.Config.Ports + ports := devbox.Spec.Config.Ports // TODO: add extra ports to pod, currently not support // ports = append(ports, devbox.Spec.NetworkSpec.ExtraPorts...) - envs := runtime.Spec.Config.Env - envs = append(envs, devbox.Spec.ExtraEnvs...) + envs := devbox.Spec.Config.Env envs = append(envs, helper.GenerateDevboxEnvVars(devbox, nextCommitHistory)...) //get image name var imageName string if r.DebugMode { - imageName = runtime.Spec.Config.Image + imageName = devbox.Spec.Image } else { - imageName = helper.GetLastSuccessCommitImageName(devbox, runtime) + imageName = helper.GetLastSuccessCommitImageName(devbox) } - volumes := runtime.Spec.Config.Volumes + volumes := devbox.Spec.Config.Volumes volumes = append(volumes, helper.GenerateSSHVolume(devbox)) - volumes = append(volumes, devbox.Spec.ExtraVolumes...) - volumeMounts := runtime.Spec.Config.VolumeMounts + volumeMounts := devbox.Spec.Config.VolumeMounts volumeMounts = append(volumeMounts, helper.GenerateSSHVolumeMounts()...) - volumeMounts = append(volumeMounts, devbox.Spec.ExtraVolumeMounts...) containers := []corev1.Container{ { @@ -554,16 +551,23 @@ func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, runt Ports: ports, VolumeMounts: volumeMounts, - WorkingDir: helper.GenerateWorkingDir(devbox, runtime), - Command: helper.GenerateCommand(devbox, runtime), - Args: helper.GenerateDevboxArgs(devbox, runtime), - Resources: helper.GenerateResourceRequirements(devbox, r.RequestCPURate, r.RequestMemoryRate, r.RequestEphemeralStorage, r.LimitEphemeralStorage), - }, + WorkingDir: helper.GetWorkingDir(devbox), + Command: helper.GetCommand(devbox), + Args: helper.GetArgs(devbox), + Resources: helper.GenerateResourceRequirements(devbox, r.RequestRate, r.EphemeralStorage)}, } terminationGracePeriodSeconds := 300 automountServiceAccountToken := false + runtimeClassName := devbox.Spec.RuntimeClassName + var runtimeClassNamePtr *string + if runtimeClassName == "" { + runtimeClassNamePtr = nil + } else { + runtimeClassNamePtr = ptr.To(runtimeClassName) + } + expectPod := &corev1.Pod{ ObjectMeta: objectMeta, Spec: corev1.PodSpec{ @@ -575,8 +579,11 @@ func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, runt Containers: containers, Volumes: volumes, - Tolerations: devbox.Spec.Tolerations, - Affinity: devbox.Spec.Affinity, + RuntimeClassName: runtimeClassNamePtr, + + NodeSelector: devbox.Spec.NodeSelector, + Tolerations: devbox.Spec.Tolerations, + Affinity: devbox.Spec.Affinity, }, } // set controller reference and finalizer @@ -604,6 +611,7 @@ func (r *DevboxReconciler) generateImageName(devbox *devboxv1alpha1.Devbox) stri // SetupWithManager sets up the controller with the Manager. func (r *DevboxReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). + WithOptions(controller.Options{MaxConcurrentReconciles: 10}). For(&devboxv1alpha1.Devbox{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Owns(&corev1.Pod{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). // enqueue request if pod spec/status is updated Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). diff --git a/controllers/devbox/internal/controller/devboxrelease_controller.go b/controllers/devbox/internal/controller/devboxrelease_controller.go index 538f67c7da25..543d3398e486 100644 --- a/controllers/devbox/internal/controller/devboxrelease_controller.go +++ b/controllers/devbox/internal/controller/devboxrelease_controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "time" reference "github.com/google/go-containerregistry/pkg/name" @@ -85,7 +86,7 @@ func (r *DevBoxReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Reques err := r.CreateReleaseTag(ctx, devboxRelease) if err != nil && errors.Is(err, registry.ErrorManifestNotFound) { logger.Info("Manifest not found, retrying", "devbox", devboxRelease.Spec.DevboxName, "newTag", devboxRelease.Spec.NewTag) - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: time.Second * 10}, nil } else if err != nil { logger.Error(err, "Failed to create release tag", "devbox", devboxRelease.Spec.DevboxName, "newTag", devboxRelease.Spec.NewTag) devboxRelease.Status.Phase = devboxv1alpha1.DevboxReleasePhaseFailed @@ -135,7 +136,7 @@ func (r *DevBoxReleaseReconciler) GetImageInfo(devbox *devboxv1alpha1.Devbox) (s if len(devbox.Status.CommitHistory) == 0 { return "", "", "", fmt.Errorf("commit history is empty") } - commitHistory := helper.GetLastSuccessCommitHistory(devbox) + commitHistory := helper.GetLastPredicatedSuccessCommitHistory(devbox) if commitHistory == nil { return "", "", "", fmt.Errorf("no successful commit history found") } diff --git a/controllers/devbox/internal/controller/helper/devbox.go b/controllers/devbox/internal/controller/helper/devbox.go index bd00faee4c8d..73fb7ab6d7ef 100644 --- a/controllers/devbox/internal/controller/helper/devbox.go +++ b/controllers/devbox/internal/controller/helper/devbox.go @@ -16,7 +16,6 @@ package helper import ( "fmt" - "log/slog" "sort" "strings" @@ -31,6 +30,7 @@ import ( "k8s.io/utils/ptr" devboxv1alpha1 "github.com/labring/sealos/controllers/devbox/api/v1alpha1" + utilsresource "github.com/labring/sealos/controllers/devbox/internal/controller/utils/resource" "github.com/labring/sealos/controllers/devbox/label" ) @@ -38,16 +38,11 @@ const ( DevBoxPartOf = "devbox" ) -func GeneratePodLabels(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Runtime) map[string]string { +func GeneratePodLabels(devbox *devboxv1alpha1.Devbox) map[string]string { labels := make(map[string]string) - if runtime.Spec.Config.Labels != nil { - for k, v := range runtime.Spec.Config.Labels { - labels[k] = v - } - } - if devbox.Spec.ExtraLabels != nil { - for k, v := range devbox.Spec.ExtraLabels { + if devbox.Spec.Config.Labels != nil { + for k, v := range devbox.Spec.Config.Labels { labels[k] = v } } @@ -62,15 +57,10 @@ func GeneratePodLabels(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Ru return labels } -func GeneratePodAnnotations(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Runtime) map[string]string { +func GeneratePodAnnotations(devbox *devboxv1alpha1.Devbox) map[string]string { annotations := make(map[string]string) - if runtime.Spec.Config.Annotations != nil { - for k, v := range runtime.Spec.Config.Annotations { - annotations[k] = v - } - } - if devbox.Spec.ExtraAnnotations != nil { - for k, v := range devbox.Spec.ExtraAnnotations { + if devbox.Spec.Config.Annotations != nil { + for k, v := range devbox.Spec.Config.Annotations { annotations[k] = v } } @@ -200,10 +190,6 @@ func podContainerID(pod *corev1.Pod) string { } return "" } - -// PredicateCommitStatus returns the commit status of the pod -// if the pod container id is empty, it means the pod is pending or has't started, we can assume the image has not been committed -// otherwise, it means the pod has been started, we can assume the image has been committed func PredicateCommitStatus(pod *corev1.Pod) devboxv1alpha1.CommitStatus { if podContainerID(pod) == "" { return devboxv1alpha1.CommitStatusPending @@ -211,85 +197,6 @@ func PredicateCommitStatus(pod *corev1.Pod) devboxv1alpha1.CommitStatus { return devboxv1alpha1.CommitStatusSuccess } -func PodMatchExpectations(expectPod *corev1.Pod, pod *corev1.Pod) bool { - if len(pod.Spec.Containers) == 0 { - slog.Info("Pod has no containers") - return false - } - container := pod.Spec.Containers[0] - expectContainer := expectPod.Spec.Containers[0] - - // Check CPU and memory limits - if container.Resources.Requests.Cpu().Cmp(*expectContainer.Resources.Requests.Cpu()) != 0 { - slog.Info("CPU requests are not equal") - return false - } - if container.Resources.Limits.Cpu().Cmp(*expectContainer.Resources.Limits.Cpu()) != 0 { - slog.Info("CPU limits are not equal") - return false - } - if container.Resources.Requests.Memory().Cmp(*expectContainer.Resources.Requests.Memory()) != 0 { - slog.Info("Memory requests are not equal") - return false - } - if container.Resources.Limits.Memory().Cmp(*expectContainer.Resources.Limits.Memory()) != 0 { - slog.Info("Memory limits are not equal") - return false - } - - // Check Ephemeral Storage changes - if container.Resources.Requests.StorageEphemeral().Cmp(*expectContainer.Resources.Requests.StorageEphemeral()) != 0 { - slog.Info("Ephemeral-Storage requests are not equal") - return false - } - if container.Resources.Limits.StorageEphemeral().Cmp(*expectContainer.Resources.Limits.StorageEphemeral()) != 0 { - slog.Info("Ephemeral-Storage limits are not equal") - return false - } - - // Check environment variables - if len(container.Env) != len(expectContainer.Env) { - return false - } - for _, env := range container.Env { - found := false - for _, expectEnv := range expectContainer.Env { - if env.Name == "SEALOS_COMMIT_IMAGE_NAME" { - found = true - break - } - if env.Name == expectEnv.Name && env.Value == expectEnv.Value { - found = true - break - } - } - if !found { - slog.Info("Environment variables are not equal", "env not found", env.Name, "env value", env.Value) - return false - } - } - - // Check ports - if len(container.Ports) != len(expectContainer.Ports) { - return false - } - for _, expectPort := range expectContainer.Ports { - found := false - for _, podPort := range container.Ports { - if expectPort.ContainerPort == podPort.ContainerPort && expectPort.Protocol == podPort.Protocol { - found = true - break - } - } - if !found { - slog.Info("Ports are not equal") - return false - } - } - - return true -} - func GenerateDevboxEnvVars(devbox *devboxv1alpha1.Devbox, nextCommitHistory *devboxv1alpha1.CommitHistory) []corev1.EnvVar { // if devbox.Spec.Squash is true, and devbox.Status.CommitHistory has success commit history, we need to set SEALOS_COMMIT_IMAGE_SQUASH to true doSquash := false @@ -347,13 +254,29 @@ func GetLastSuccessCommitHistory(devbox *devboxv1alpha1.Devbox) *devboxv1alpha1. return nil } -func GetLastSuccessCommitImageName(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Runtime) string { +func GetLastPredicatedSuccessCommitHistory(devbox *devboxv1alpha1.Devbox) *devboxv1alpha1.CommitHistory { if len(devbox.Status.CommitHistory) == 0 { - return runtime.Spec.Config.Image + return nil + } + // Sort commit history by time in descending order + sort.Slice(devbox.Status.CommitHistory, func(i, j int) bool { + return devbox.Status.CommitHistory[i].Time.After(devbox.Status.CommitHistory[j].Time.Time) + }) + for _, commit := range devbox.Status.CommitHistory { + if commit.PredicatedStatus == devboxv1alpha1.CommitStatusSuccess { + return commit + } + } + return nil +} + +func GetLastSuccessCommitImageName(devbox *devboxv1alpha1.Devbox) string { + if len(devbox.Status.CommitHistory) == 0 { + return devbox.Spec.Image } commit := GetLastSuccessCommitHistory(devbox) if commit == nil { - return runtime.Spec.Config.Image + return devbox.Spec.Image } return commit.Image } @@ -398,76 +321,63 @@ func GenerateSSHVolume(devbox *devboxv1alpha1.Devbox) corev1.Volume { } } -func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, - requestCPURate, requestMemoryRate float64, - requestEphemeralStorage, limitEphemeralStorage string, -) corev1.ResourceRequirements { +// GenerateResourceRequirements generates the resource requirements for the Devbox pod +func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, requestRate utilsresource.RequestRate, ephemeralStorage utilsresource.EphemeralStorage) corev1.ResourceRequirements { return corev1.ResourceRequirements{ - Requests: calculateResourceRequest( - corev1.ResourceList{ - corev1.ResourceCPU: devbox.Spec.Resource["cpu"], - corev1.ResourceMemory: devbox.Spec.Resource["memory"], - corev1.ResourceEphemeralStorage: resource.MustParse(requestEphemeralStorage), - }, - requestCPURate, requestMemoryRate, - ), - Limits: corev1.ResourceList{ - corev1.ResourceCPU: devbox.Spec.Resource["cpu"], - corev1.ResourceMemory: devbox.Spec.Resource["memory"], - corev1.ResourceEphemeralStorage: resource.MustParse(limitEphemeralStorage), - }, + Limits: calculateResourceLimit(devbox.Spec.Resource, ephemeralStorage), + Requests: calculateResourceRequest(devbox.Spec.Resource, requestRate, ephemeralStorage), } } -func IsExceededQuotaError(err error) bool { - return strings.Contains(err.Error(), "exceeded quota") +func calculateResourceLimit(original corev1.ResourceList, ephemeralStorage utilsresource.EphemeralStorage) corev1.ResourceList { + limit := original.DeepCopy() + // If ephemeral storage limit is not set, set it to default limit + if l, ok := limit[corev1.ResourceEphemeralStorage]; !ok { + limit[corev1.ResourceEphemeralStorage] = ephemeralStorage.DefaultLimit + } else { + // Check if the resource limit for ephemeral storage is set and compare it, if it is exceeded the maximum limit, set it to maximum limit + if l.AsApproximateFloat64() > ephemeralStorage.MaximumLimit.AsApproximateFloat64() { + limit[corev1.ResourceEphemeralStorage] = ephemeralStorage.MaximumLimit + } + } + return limit } -func calculateResourceRequest(limit corev1.ResourceList, requestCPURate, requestMemoryRate float64) corev1.ResourceList { - if limit == nil { - return nil - } - request := make(corev1.ResourceList) +func calculateResourceRequest(original corev1.ResourceList, requestRate utilsresource.RequestRate, ephemeralStorage utilsresource.EphemeralStorage) corev1.ResourceList { + // deep copy limit to request, only cpu and memory are calculated + request := original.DeepCopy() // Calculate CPU request - if cpu, ok := limit[corev1.ResourceCPU]; ok { + if cpu, ok := original[corev1.ResourceCPU]; ok { cpuValue := cpu.AsApproximateFloat64() - cpuRequest := cpuValue / requestCPURate + cpuRequest := cpuValue / requestRate.CPU request[corev1.ResourceCPU] = *resource.NewMilliQuantity(int64(cpuRequest*1000), resource.DecimalSI) } // Calculate memory request - if memory, ok := limit[corev1.ResourceMemory]; ok { + if memory, ok := original[corev1.ResourceMemory]; ok { memoryValue := memory.AsApproximateFloat64() - memoryRequest := memoryValue / requestMemoryRate + memoryRequest := memoryValue / requestRate.Memory request[corev1.ResourceMemory] = *resource.NewQuantity(int64(memoryRequest), resource.BinarySI) } - - if ephemeralStorage, ok := limit[corev1.ResourceEphemeralStorage]; ok { - request[corev1.ResourceEphemeralStorage] = ephemeralStorage - } - + // Set ephemeral storage request to default request + request[corev1.ResourceEphemeralStorage] = ephemeralStorage.DefaultRequest return request } -// GenerateWorkingDir generates the working directory for the Devbox pod -func GenerateWorkingDir(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Runtime) string { - if devbox.Spec.WorkingDir != "" { - return devbox.Spec.WorkingDir - } - return runtime.Spec.Config.WorkingDir +// GetWorkingDir get the working directory for the Devbox pod +func GetWorkingDir(devbox *devboxv1alpha1.Devbox) string { + return devbox.Spec.Config.WorkingDir } -// GenerateCommand generates the command for the Devbox pod -func GenerateCommand(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Runtime) []string { - if len(devbox.Spec.Command) != 0 { - return devbox.Spec.Command - } - return runtime.Spec.Config.Command +// GetCommand get the command for the Devbox pod +func GetCommand(devbox *devboxv1alpha1.Devbox) []string { + return devbox.Spec.Config.Command } -// GenerateDevboxArgs generates the arguments for the Devbox pod -func GenerateDevboxArgs(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alpha1.Runtime) []string { - if len(devbox.Spec.Args) != 0 { - return devbox.Spec.Args - } - return runtime.Spec.Config.Args +// GetArgs get the arguments for the Devbox pod +func GetArgs(devbox *devboxv1alpha1.Devbox) []string { + return devbox.Spec.Config.Args +} + +func IsExceededQuotaError(err error) bool { + return strings.Contains(err.Error(), "exceeded quota") } diff --git a/controllers/devbox/internal/controller/utils/matcher/matcher.go b/controllers/devbox/internal/controller/utils/matcher/matcher.go new file mode 100644 index 000000000000..dfad8e59f9f6 --- /dev/null +++ b/controllers/devbox/internal/controller/utils/matcher/matcher.go @@ -0,0 +1,154 @@ +// Copyright © 2024 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package matcher + +import ( + "log/slog" + + corev1 "k8s.io/api/core/v1" +) + +type PodMatcher interface { + Match(expectPod *corev1.Pod, pod *corev1.Pod) bool +} + +type ResourceMatcher struct{} + +func (r ResourceMatcher) Match(expectPod *corev1.Pod, pod *corev1.Pod) bool { + if len(pod.Spec.Containers) == 0 { + slog.Info("Pod has no containers") + return false + } + container := pod.Spec.Containers[0] + expectContainer := expectPod.Spec.Containers[0] + + if container.Resources.Requests.Cpu().Cmp(*expectContainer.Resources.Requests.Cpu()) != 0 { + slog.Info("CPU requests are not equal") + return false + } + if container.Resources.Limits.Cpu().Cmp(*expectContainer.Resources.Limits.Cpu()) != 0 { + slog.Info("CPU limits are not equal") + return false + } + if container.Resources.Requests.Memory().Cmp(*expectContainer.Resources.Requests.Memory()) != 0 { + slog.Info("Memory requests are not equal") + return false + } + if container.Resources.Limits.Memory().Cmp(*expectContainer.Resources.Limits.Memory()) != 0 { + slog.Info("Memory limits are not equal") + return false + } + return true +} + +type EphemeralStorageMatcher struct{} + +func (e EphemeralStorageMatcher) Match(expectPod *corev1.Pod, pod *corev1.Pod) bool { + if len(pod.Spec.Containers) == 0 { + slog.Info("Pod has no containers") + return false + } + container := pod.Spec.Containers[0] + expectContainer := expectPod.Spec.Containers[0] + + if container.Resources.Limits.StorageEphemeral().Cmp(*expectContainer.Resources.Limits.StorageEphemeral()) != 0 { + slog.Info("Ephemeral-Storage limits are not equal") + return false + } + if container.Resources.Requests.StorageEphemeral().Cmp(*expectContainer.Resources.Requests.StorageEphemeral()) != 0 { + slog.Info("Ephemeral-Storage requests are not equal") + return false + } + return true +} + +type EnvVarMatcher struct{} + +func (e EnvVarMatcher) Match(expectPod *corev1.Pod, pod *corev1.Pod) bool { + if len(pod.Spec.Containers) == 0 { + slog.Info("Pod has no containers") + return false + } + container := pod.Spec.Containers[0] + expectContainer := expectPod.Spec.Containers[0] + + if len(container.Env) != len(expectContainer.Env) { + slog.Info("Environment variable count mismatch") + return false + } + + for _, env := range container.Env { + found := false + for _, expectEnv := range expectContainer.Env { + if env.Name == "SEALOS_COMMIT_IMAGE_NAME" { + found = true + break + } + if env.Name == expectEnv.Name && env.Value == expectEnv.Value { + found = true + break + } + } + if !found { + slog.Info("Environment variables are not equal", "env not found", env.Name, "env value", env.Value) + return false + } + } + return true +} + +type PortMatcher struct{} + +func (p PortMatcher) Match(expectPod *corev1.Pod, pod *corev1.Pod) bool { + if len(pod.Spec.Containers) == 0 { + slog.Info("Pod has no containers") + return false + } + container := pod.Spec.Containers[0] + expectContainer := expectPod.Spec.Containers[0] + + if len(container.Ports) != len(expectContainer.Ports) { + slog.Info("Port count mismatch") + return false + } + + for _, expectPort := range expectContainer.Ports { + found := false + for _, podPort := range container.Ports { + if expectPort.ContainerPort == podPort.ContainerPort && expectPort.Protocol == podPort.Protocol { + found = true + break + } + } + if !found { + slog.Info("Ports are not equal") + return false + } + } + return true +} + +// PredicateCommitStatus returns the commit status of the pod +// if the pod container id is empty, it means the pod is pending or has't started, we can assume the image has not been committed +// otherwise, it means the pod has been started, we can assume the image has been committed + +func PodMatchExpectations(expectPod *corev1.Pod, pod *corev1.Pod, matchers ...PodMatcher) bool { + for _, matcher := range matchers { + if !matcher.Match(expectPod, pod) { + return false + } + } + return true +} diff --git a/controllers/devbox/internal/controller/helper/devbox_test.go b/controllers/devbox/internal/controller/utils/matcher/matcher_test.go similarity index 95% rename from controllers/devbox/internal/controller/helper/devbox_test.go rename to controllers/devbox/internal/controller/utils/matcher/matcher_test.go index f3563b743fb6..1317615196af 100644 --- a/controllers/devbox/internal/controller/helper/devbox_test.go +++ b/controllers/devbox/internal/controller/utils/matcher/matcher_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package helper +package matcher import ( "testing" @@ -156,9 +156,16 @@ func TestPodMatchExpectations(t *testing.T) { }, } + matchers := []PodMatcher{ + ResourceMatcher{}, + EnvVarMatcher{}, + PortMatcher{}, + EphemeralStorageMatcher{}, + } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := PodMatchExpectations(expectPod, tt.pod) + result := PodMatchExpectations(expectPod, tt.pod, matchers...) if result != tt.expected { t.Errorf("CheckPodConsistency() = %v, expected %v", result, tt.expected) } diff --git a/controllers/devbox/internal/controller/utils/resource/resource.go b/controllers/devbox/internal/controller/utils/resource/resource.go new file mode 100644 index 000000000000..3e0e47813602 --- /dev/null +++ b/controllers/devbox/internal/controller/utils/resource/resource.go @@ -0,0 +1,30 @@ +// Copyright © 2024 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resource + +import ( + "k8s.io/apimachinery/pkg/api/resource" +) + +type RequestRate struct { + CPU float64 + Memory float64 +} + +type EphemeralStorage struct { + DefaultRequest resource.Quantity + DefaultLimit resource.Quantity + MaximumLimit resource.Quantity +} diff --git a/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl b/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl index d050afa43b6e..4c3bd5aa11dc 100644 --- a/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl @@ -277,29 +277,29 @@ rules: - patch - update - apiGroups: - - '' - resources: - - resourcequotas - verbs: - - get - - patch - - update - - create - - delete - - watch - - list + - '' + resources: + - resourcequotas + verbs: + - get + - patch + - update + - create + - delete + - watch + - list - apiGroups: - - '' - resources: - - resourcequotas/status - verbs: - - get - - patch - - update - - create - - delete - - watch - - list + - '' + resources: + - resourcequotas/status + verbs: + - get + - patch + - update + - create + - delete + - watch + - list --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/controllers/pkg/common/account.go b/controllers/pkg/common/account.go index ddc0813ecddd..06a0f790d13c 100644 --- a/controllers/pkg/common/account.go +++ b/controllers/pkg/common/account.go @@ -15,3 +15,8 @@ package common type Type int + +const ( + Consumption Type = iota + SubConsumption +) diff --git a/controllers/pkg/database/cockroach/accountv2.go b/controllers/pkg/database/cockroach/accountv2.go index f331269e41df..92b6065e89fa 100644 --- a/controllers/pkg/database/cockroach/accountv2.go +++ b/controllers/pkg/database/cockroach/accountv2.go @@ -1188,3 +1188,20 @@ func (c *Cockroach) GetUserRealNameInfoByUserID(userID string) (*types.UserRealN } return &userRealNameInfo, nil } + +func (c *Cockroach) GetEnterpriseRealNameInfoByUserID(userID string) (*types.EnterpriseRealNameInfo, error) { + // get user info + ops := &types.UserQueryOpts{ID: userID} + user, err := c.GetUserCr(ops) + + if err != nil { + return nil, fmt.Errorf("failed to get user: %v", err) + } + + // get user realname info + var enterpriseRealNameInfo types.EnterpriseRealNameInfo + if err := c.DB.Where(&types.EnterpriseRealNameInfo{UserUID: user.UserUID}).First(&enterpriseRealNameInfo).Error; err != nil { + return nil, fmt.Errorf("failed to get enterprise real name info: %w", err) + } + return &enterpriseRealNameInfo, nil +} diff --git a/controllers/pkg/database/interface.go b/controllers/pkg/database/interface.go index 36e474a41ea5..09bc64591957 100644 --- a/controllers/pkg/database/interface.go +++ b/controllers/pkg/database/interface.go @@ -47,19 +47,21 @@ type CVM interface { type Account interface { GetBillingLastUpdateTime(owner string, _type common.Type) (bool, time.Time, error) + GetOwnersRecentUpdates(ownerList []string, checkTime time.Time) ([]string, error) + GetTimeUsedNamespaceList(startTime, endTime time.Time) ([]string, error) SaveBillings(billing ...*resources.Billing) error SaveObjTraffic(obs ...*types.ObjectStorageTraffic) error GetAllLatestObjTraffic(startTime, endTime time.Time) ([]types.ObjectStorageTraffic, error) HandlerTimeObjBucketSentTraffic(startTime, endTime time.Time, bucket string) (int64, error) GetTimeObjBucketBucket(startTime, endTime time.Time) ([]string, error) GetUnsettingBillingHandler(owner string) ([]resources.BillingHandler, error) - UpdateBillingStatus(orderID string, status resources.BillingStatus) error + UpdateBillingStatus(orderIDs []string, status resources.BillingStatus) error GetUpdateTimeForCategoryAndPropertyFromMetering(category string, property string) (time.Time, error) GetAllPayment() ([]resources.Billing, error) InitDefaultPropertyTypeLS() error SavePropertyTypes(types []resources.PropertyType) error GetBillingCount(accountType common.Type, startTime, endTime time.Time) (count, amount int64, err error) - GenerateBillingData(startTime, endTime time.Time, prols *resources.PropertyTypeLS, namespaces []string, owner string) (orderID []string, amount int64, err error) + GenerateBillingData(startTime, endTime time.Time, prols *resources.PropertyTypeLS, ownerToNS map[string][]string) (map[string][]*resources.Billing, error) InsertMonitor(ctx context.Context, monitors ...*resources.Monitor) error GetDistinctMonitorCombinations(startTime, endTime time.Time) ([]resources.Monitor, error) DropMonitorCollectionsOlderThan(days int) error diff --git a/controllers/pkg/database/mongo/account.go b/controllers/pkg/database/mongo/account.go index e8dc8ce38ef7..2aa6ac5c94e4 100644 --- a/controllers/pkg/database/mongo/account.go +++ b/controllers/pkg/database/mongo/account.go @@ -149,6 +149,87 @@ func (m *mongoDB) GetBillingLastUpdateTime(owner string, _type common.Type) (boo return false, time.Time{}, fmt.Errorf("failed to convert time field to primitive.DateTime: %v", result["time"]) } +func (m *mongoDB) GetOwnersRecentUpdates(ownerList []string, checkTime time.Time) ([]string, error) { + // MongoDB filter + filter := bson.M{ + "owner": bson.M{"$in": ownerList}, + "type": common.Consumption, + "app_type": bson.M{ + "$nin": []int{int(resources.AppType[resources.CVM]), int(resources.AppType[resources.LLMToken])}, + }, + } + + // Aggregate query: Group by owner to get the latest time + pipeline := mongo.Pipeline{ + {{Key: "$match", Value: filter}}, + {{Key: "$sort", Value: bson.D{{Key: "owner", Value: 1}, {Key: "time", Value: -1}}}}, // Sort by owner first, then by time in descending order + {{Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$owner"}, + {Key: "lastUpdateTime", Value: bson.D{{Key: "$first", Value: "$time"}}}, // fetch latest Time + }}}, + } + // execute aggregate query + cursor, err := m.getBillingCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return nil, fmt.Errorf("failed to execute aggregate query: %w", err) + } + defer cursor.Close(context.Background()) + + // get all the data out at once + var results []struct { + Owner string `bson:"_id"` + LastUpdateRaw primitive.DateTime `bson:"lastUpdateTime"` + } + if err := cursor.All(context.Background(), &results); err != nil { + return nil, fmt.Errorf("failed to decode cursor: %w", err) + } + + // use map to store query results + latestUpdates := make(map[string]time.Time, len(results)) + for _, result := range results { + latestUpdates[result.Owner] = result.LastUpdateRaw.Time() + } + + // **In-memory processing: Filters owners that have been updated since checkTime** + var updatedOwners []string + for _, owner := range ownerList { + lastUpdateTime, exists := latestUpdates[owner] + if exists && (lastUpdateTime.After(checkTime) || lastUpdateTime.Equal(checkTime)) { + updatedOwners = append(updatedOwners, owner) + } + } + return updatedOwners, nil +} + +func (m *mongoDB) GetTimeUsedNamespaceList(startTime, endTime time.Time) ([]string, error) { + pipeline := mongo.Pipeline{ + {{Key: "$match", Value: bson.D{{Key: "time", Value: bson.D{{Key: "$gte", Value: startTime}, {Key: "$lt", Value: endTime}}}}}}, + {{Key: "$group", Value: bson.D{{Key: "_id", Value: "$category"}}}}, + {{Key: "$project", Value: bson.D{{Key: "_id", Value: 0}, {Key: "namespace", Value: "$_id"}}}}, + } + cursor, err := m.getMonitorCollection(startTime).Aggregate(context.Background(), pipeline) + if err != nil { + return nil, fmt.Errorf("aggregate error: %v", err) + } + defer cursor.Close(context.Background()) + + var namespaces []string + for cursor.Next(context.Background()) { + var result struct { + Namespace string `bson:"namespace"` + } + err := cursor.Decode(&result) + if err != nil { + return nil, fmt.Errorf("decode error: %v", err) + } + namespaces = append(namespaces, result.Namespace) + } + if err = cursor.Err(); err != nil { + return nil, fmt.Errorf("cursor error: %v", err) + } + return namespaces, nil +} + func (m *mongoDB) GetUnsettingBillingHandler(owner string) ([]resources.BillingHandler, error) { filter := bson.M{ "owner": owner, @@ -178,15 +259,15 @@ func (m *mongoDB) GetUnsettingBillingHandler(owner string) ([]resources.BillingH return results, nil } -func (m *mongoDB) UpdateBillingStatus(orderID string, status resources.BillingStatus) error { +func (m *mongoDB) UpdateBillingStatus(orderIDs []string, status resources.BillingStatus) error { // create a query filter - filter := bson.M{"order_id": orderID} + filter := bson.M{"order_id": bson.M{"$in": orderIDs}} update := bson.M{ "$set": bson.M{ "status": status, }, } - _, err := m.getBillingCollection().UpdateOne(context.Background(), filter, update) + _, err := m.getBillingCollection().UpdateMany(context.Background(), filter, update) if err != nil { return fmt.Errorf("update error: %v", err) } @@ -421,185 +502,234 @@ func (m *mongoDB) SavePropertyTypes(types []resources.PropertyType) error { return err } -func (m *mongoDB) GenerateBillingData(startTime, endTime time.Time, prols *resources.PropertyTypeLS, namespaces []string, owner string) (orderID []string, amount int64, err error) { - minutes := endTime.Sub(startTime).Minutes() - - groupStage := bson.D{ - primitive.E{Key: "_id", Value: bson.D{{Key: "type", Value: "$type"}, {Key: "name", Value: "$name"}, {Key: "category", Value: "$category"}, {Key: "parent_type", Value: "$parent_type"}, {Key: "parent_name", Value: "$parent_name"}}}, - primitive.E{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, +func (m *mongoDB) GenerateBillingData(startTime, endTime time.Time, prols *resources.PropertyTypeLS, ownerToNS map[string][]string) (map[string][]*resources.Billing, error) { + ownerMonitors, err := m.FetchOwnerMonitorRecords(startTime, endTime, ownerToNS) + if err != nil { + return nil, fmt.Errorf("failed to fetch monitor records: %v", err) } - - projectStage := bson.D{ - primitive.E{Key: "_id", Value: 0}, - primitive.E{Key: "type", Value: "$_id.type"}, - primitive.E{Key: "name", Value: "$_id.name"}, - primitive.E{Key: "parent_type", Value: "$_id.parent_type"}, - primitive.E{Key: "parent_name", Value: "$_id.parent_name"}, - primitive.E{Key: "category", Value: "$_id.category"}, + var ownerBillings = make(map[string][]*resources.Billing) + for owner, monitors := range ownerMonitors { + billings, err := GenerateBillingDataFromRecords(monitors, prols, startTime, endTime, owner) + if err != nil { + return nil, fmt.Errorf("failed to generate billing data: %v", err) + } + ownerBillings[owner] = billings } + return ownerBillings, nil +} - // initialize the used phase - usedStage := bson.M{} - - // Build the $group and $project phases dynamically from EnumMap - for key, value := range prols.EnumMap { - keyStr := strconv.Itoa(int(key)) - - // $max - $min; - // When max is not zero, the minimum value other than the zero value is used to prevent some data from obtaining a value in special cases - // max-min=0 if the hour has only one data piece or no data piece - if value.PriceType == resources.DIF { - // for non 0 $min - minWithCondition := bson.D{ - {Key: "$min", Value: bson.D{ - {Key: "$cond", Value: bson.A{ - bson.D{{Key: "$eq", Value: bson.A{"$used." + keyStr, 0}}}, - nil, // 将0值排除在外 - "$used." + keyStr, - }}, - }}, - } - - groupStage = append(groupStage, - primitive.E{Key: keyStr + "_max", Value: bson.D{{Key: "$max", Value: "$used." + keyStr}}}, // 正常计算$max - primitive.E{Key: keyStr + "_min", Value: minWithCondition}, - ) - - // added to the used phase - usedStage[keyStr] = bson.D{{Key: "$subtract", Value: bson.A{ - "$" + keyStr + "_max", - "$" + keyStr + "_min", - }}} - continue - } - if value.PriceType == resources.SUM { - groupStage = append(groupStage, primitive.E{Key: keyStr, Value: bson.D{{Key: "$sum", Value: "$used." + keyStr}}}) - usedStage[keyStr] = bson.D{{Key: "$toInt", Value: "$" + keyStr}} - continue +func (m *mongoDB) FetchOwnerMonitorRecords(startTime, endTime time.Time, ownerToNS map[string][]string) (map[string][]resources.Monitor, error) { + // collect all namespaces to avoid repetition + nsSet := make(map[string]struct{}) + for _, nsList := range ownerToNS { + for _, ns := range nsList { + nsSet[ns] = struct{}{} } - groupStage = append(groupStage, primitive.E{Key: keyStr, Value: bson.D{{Key: "$sum", Value: "$used." + keyStr}}}) - usedStage[keyStr] = bson.D{{Key: "$toInt", Value: bson.D{{Key: "$round", Value: bson.D{{Key: "$divide", Value: bson.A{ - "$" + keyStr, minutes}}}}}}} + } + namespaces := make([]string, 0, len(nsSet)) + for ns := range nsSet { + namespaces = append(namespaces, ns) } - // add the used phase to the $project phase - projectStage = append(projectStage, primitive.E{Key: "used", Value: usedStage}) - - // construction-pipeline - pipeline := mongo.Pipeline{ - {{Key: "$match", Value: bson.D{{Key: "time", Value: bson.D{{Key: "$gte", Value: startTime}, {Key: "$lt", Value: endTime}}}, {Key: "category", Value: bson.D{{Key: "$in", Value: namespaces}}}}}}, - {{Key: "$group", Value: groupStage}}, - {{Key: "$project", Value: projectStage}}, + // get all matching monitor records from mongodb + collection := m.getMonitorCollection(startTime) + filter := bson.M{ + "time": bson.M{"$gte": startTime, "$lt": endTime}, + "category": bson.M{"$in": namespaces}, // 查询所有涉及的 namespaces } - cursor, err := m.getMonitorCollection(startTime).Aggregate(context.Background(), pipeline) + cursor, err := collection.Find(context.Background(), filter) if err != nil { - return nil, 0, fmt.Errorf("aggregate error: %v", err) + return nil, fmt.Errorf("failed to find monitor records: %w", err) } defer cursor.Close(context.Background()) - var appCostsMap = make(map[string]map[string][]resources.AppCost) - // map[ns/type]int64 - var nsTypeAmount = make(map[string]int64) - - for cursor.Next(context.Background()) { - var result struct { - Type uint8 `bson:"type"` - Namespace string `bson:"category"` - Name string `bson:"name"` - ParentType uint8 `bson:"parent_type"` - ParentName string `bson:"parent_name"` - Used resources.EnumUsedMap `bson:"used"` + // reading mongodb data + var allRecords []resources.Monitor + if err := cursor.All(context.Background(), &allRecords); err != nil { + return nil, fmt.Errorf("failed to decode monitor records: %w", err) + } + + // build the mapping of owner monitor data + ownerMonitorRecords := make(map[string][]resources.Monitor) + for _, record := range allRecords { + for owner, nsList := range ownerToNS { + // Only the records of the namespace that belong to the owner are saved + for _, ns := range nsList { + if record.Category == ns { + ownerMonitorRecords[owner] = append(ownerMonitorRecords[owner], record) + break // avoid duplicate additions + } + } } + } - err := cursor.Decode(&result) - if err != nil { - return nil, 0, fmt.Errorf("decode error: %v", err) - } + return ownerMonitorRecords, nil +} - //TODO delete - //logger.Info("generate billing data", "result", result) +func GenerateBillingDataFromRecords(records []resources.Monitor, prols *resources.PropertyTypeLS, startTime, endTime time.Time, owner string) (billings []*resources.Billing, err error) { + // Calculate the interval (minutes) to ensure that the divisor is not 0 + minutes := math.Max(endTime.Sub(startTime).Minutes(), 1) - if _, ok := appCostsMap[result.Namespace]; !ok { - appCostsMap[result.Namespace] = make(map[string][]resources.AppCost) + // 存储分组后的数据 + aggregatedMap := make(map[string]*struct { + resources.Monitor + UsedValues map[uint8][]int64 + Count int64 + }) + + // 分组 key 生成规则 + genGroupKey := func(rec resources.Monitor) string { + return fmt.Sprintf("%s/%d/%s", rec.Category, rec.Type, rec.Name) + } + + // 遍历所有记录,按分组键聚合 + for _, rec := range records { + key := genGroupKey(rec) + if _, ok := aggregatedMap[key]; !ok { + aggregatedMap[key] = &struct { + resources.Monitor + UsedValues map[uint8][]int64 + Count int64 + }{ + Monitor: rec, + UsedValues: make(map[uint8][]int64), + Count: 0, + } } - strType := strconv.Itoa(int(result.Type)) - if _, ok := appCostsMap[result.Namespace][strType]; !ok { - appCostsMap[result.Namespace][strType] = make([]resources.AppCost, 0) + aggregatedMap[key].Count++ + for k, v := range rec.Used { + //aggregatedMap[key].UsedValues[k] = append(aggregatedMap[key].UsedValues[k], v) + if _, exists := aggregatedMap[key].UsedValues[k]; !exists { + aggregatedMap[key].UsedValues[k] = make([]int64, 0, len(records)) + } + aggregatedMap[key].UsedValues[k] = append(aggregatedMap[key].UsedValues[k], v) + } + } + + // 存储最终计费数据 + // map[namespace]map[app_type | parent_type/parent_name][]resources.AppCost + appCostsMap := make(map[string]map[string][]resources.AppCost) + nsTypeAmount := make(map[string]map[string]int64) + + calculateFinalUsed := func(values map[uint8][]int64, prols *resources.PropertyTypeLS, minutes float64) map[uint8]int64 { + finalUsed := make(map[uint8]int64) + for propKey, vals := range values { + if prop, ok := prols.EnumMap[propKey]; ok { + finalUsed[propKey] = computeUsedValue(vals, prop, minutes) + } } + return finalUsed + } + // 计算最终 Used 数据 + for _, agg := range aggregatedMap { + finalUsed := calculateFinalUsed(agg.UsedValues, prols, minutes) + // 计算费用 appCost := resources.AppCost{ - Type: result.Type, - Used: result.Used, - Name: result.Name, + Type: agg.Type, + Name: agg.Name, + Used: finalUsed, UsedAmount: make(map[uint8]int64), } - // Calculate the amount and set the used value - for property := range result.Used { - if prop, ok := prols.EnumMap[property]; ok { + var totalAmount int64 + for propKey, usedVal := range finalUsed { + if prop, ok := prols.EnumMap[propKey]; ok { if prop.UnitPrice > 0 { - appCost.UsedAmount[property] = int64(math.Ceil(float64(result.Used[property]) * prop.UnitPrice)) - appCost.Amount += appCost.UsedAmount[property] + feeFloat := float64(usedVal) * prop.UnitPrice + if feeFloat > math.MaxInt64 { + return nil, fmt.Errorf("fee calculation overflow: %f", feeFloat) + } + fee := int64(math.Ceil(feeFloat)) + appCost.UsedAmount[propKey] = fee + totalAmount += fee } } } - if appCost.Amount == 0 { + if totalAmount == 0 { continue } - key := result.Namespace + "/" + strType - if result.ParentType != 0 && result.ParentName != "" { - key = result.Namespace + "/" + strconv.Itoa(int(result.ParentType)) + "/" + result.ParentName + appCost.Amount = totalAmount + groupKey := strconv.Itoa(int(agg.Type)) + if agg.ParentType != 0 && agg.ParentName != "" { + groupKey = strconv.Itoa(int(agg.ParentType)) + "/" + agg.ParentName + } + ns := agg.Category + if _, ok := nsTypeAmount[ns]; !ok { + nsTypeAmount[ns] = make(map[string]int64) + } + nsTypeAmount[ns][groupKey] += totalAmount + + if _, ok := appCostsMap[ns]; !ok { + appCostsMap[ns] = make(map[string][]resources.AppCost) } - nsTypeAmount[key] += appCost.Amount - appCostsMap[result.Namespace][key] = append(appCostsMap[result.Namespace][key], appCost) + appCostsMap[ns][groupKey] = append(appCostsMap[ns][groupKey], appCost) } + billings = make([]*resources.Billing, 0) + // 生成 Billing 数据 for ns, appCostMap := range appCostsMap { - for tp, appCost := range appCostMap { - amountt := nsTypeAmount[tp] - if amountt == 0 { + for tp, appCostList := range appCostMap { + amount := nsTypeAmount[ns][tp] + if amount <= 0 { continue } id, err := gonanoid.New(12) if err != nil { - return nil, 0, fmt.Errorf("generate billing id error: %v", err) + return nil, fmt.Errorf("generate billing id error: %v", err) } - // tp = ns/type/parentName && parentName not contain "/" - appType, appName := 0, "" - switch strings.Count(tp, "/") { - case 1: - appType, _ = strconv.Atoi(strings.Split(tp, "/")[1]) - case 2: - appType, _ = strconv.Atoi(strings.Split(tp, "/")[1]) - appName = strings.Split(tp, "/")[2] + parts := strings.Split(tp, "/") + appType, _ := strconv.Atoi(parts[0]) + appName := "" + if len(parts) > 1 { + appName = parts[1] } - billing := resources.Billing{ + billings = append(billings, &resources.Billing{ OrderID: id, Type: Consumption, Namespace: ns, AppType: uint8(appType), AppName: appName, - AppCosts: appCost, - Amount: amountt, + AppCosts: appCostList, + Amount: amount, Owner: owner, Time: endTime, Status: resources.Settled, - } - amount += amountt - orderID = append(orderID, id) - // Insert the billing document - _, err = m.getBillingCollection().InsertOne(context.Background(), billing) - if err != nil { - return nil, 0, fmt.Errorf("insert error: %v", err) - } - //TODO delete - //logger.Info("generate billing data", "billing", billing) + }) } } + return billings, nil +} - if err = cursor.Err(); err != nil { - return nil, 0, fmt.Errorf("cursor error: %v", err) +func computeUsedValue(usedValues []int64, prop resources.PropertyType, minutes float64) int64 { + switch prop.PriceType { + case resources.DIF: + var maxVal int64 = -math.MaxInt64 + var minVal int64 = math.MaxInt64 + for _, v := range usedValues { + if v > maxVal { + maxVal = v + } + if v != 0 && v < minVal { + minVal = v + } + } + if maxVal > minVal { + return maxVal - minVal + } + return 0 + case resources.SUM: + var sum int64 + for _, v := range usedValues { + sum += v + } + return sum + default: + var sum int64 + for _, v := range usedValues { + sum += v + } + return int64(math.Round(float64(sum) / minutes)) } - return orderID, amount, nil } func (m *mongoDB) GetUpdateTimeForCategoryAndPropertyFromMetering(category string, property string) (time.Time, error) { diff --git a/controllers/pkg/database/mongo/account_test.go b/controllers/pkg/database/mongo/account_test.go index 82f3f94f121b..46d3ef147b4a 100644 --- a/controllers/pkg/database/mongo/account_test.go +++ b/controllers/pkg/database/mongo/account_test.go @@ -294,27 +294,6 @@ info generate billing data used {2 ns-7uyfrr47 pay-xy map[0:325 1:166 2:0]} cpu: 500m memory: 256Mi */ -func TestMongoDB_GenerateBillingData(t *testing.T) { - dbCTX := context.Background() - - m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) - if err != nil { - t.Errorf("failed to connect mongo: error = %v", err) - } - defer func() { - if err = m.Disconnect(dbCTX); err != nil { - t.Errorf("failed to disconnect mongo: error = %v", err) - } - }() - queryTime := time.Now().UTC() - - ids, amount, err := m.GenerateBillingData(queryTime.Add(-1*time.Hour), queryTime, resources.DefaultPropertyTypeLS, []string{"ns-7uyfrr47", "ns-1jc12uh6", "ns-ezplle8l"}, "1jc12uh6") - if err != nil { - t.Fatalf("failed to generate billing data: %v", err) - } - t.Logf("generate billing data used %v", amount) - t.Logf("generate billing data used %v", ids) -} func TestMongoDB_SetPropertyTypeLS(t *testing.T) { dbCTX := context.Background() @@ -479,3 +458,72 @@ func Test_mongoDB_GetTimeObjBucketBucket(t *testing.T) { t.Logf("bucket: %#+v", bucket) } } + +func Test_mongoDB_GetTimeUsedOwnerList(t *testing.T) { + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, "") + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + owners, err := m.GetTimeUsedNamespaceList(time.Now().UTC().Add(-time.Hour), time.Now().UTC()) + if err != nil { + t.Fatalf("failed to get time used owner list: %v", err) + } + t.Logf("get time used owner list success: %v", owners) +} + +func Test_mongoDB_GenerateBillingData(t *testing.T) { + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGO_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + prols := resources.DefaultPropertyTypeLS + ownerToNS := map[string][]string{ + "ax1uut8w": {"ns-tnw80mhk", "ns-ax1uut8w"}, + } + billings, err := m.GenerateBillingData(time.Now().UTC().Add(-time.Hour), time.Now().UTC(), prols, ownerToNS) + if err != nil { + t.Fatalf("failed to generate billing data: %v", err) + } + for _, billing := range billings { + for _, bill := range billing { + t.Logf("%+v\n", bill) + } + } +} + +func Test_mongoDB_GetOwnersWithoutRecentUpdates(t *testing.T) { + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, "") + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + now := time.Now().UTC() + endHourTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local).UTC() + owners, err := m.GetOwnersRecentUpdates([]string{"nfhmc74p"}, endHourTime) + if err != nil { + t.Fatalf("failed to get owners without recent updates: %v", err) + } + t.Logf("get owners without recent updates success: %v", owners) +} diff --git a/controllers/pkg/resources/resources.go b/controllers/pkg/resources/resources.go index 542798b12be5..79ba47529cd7 100644 --- a/controllers/pkg/resources/resources.go +++ b/controllers/pkg/resources/resources.go @@ -135,7 +135,7 @@ type Billing struct { // if type = Transfer, then transfer is not nil Transfer *Transfer `json:"transfer" bson:"transfer,omitempty"` Detail string `json:"detail" bson:"detail,omitempty"` - UserUID uuid.UUID `json:"user_uid" bson:"user_uid,omitempty"` + //UserUID uuid.UUID `json:"user_uid" bson:"user_uid,omitempty"` } type Payment struct { diff --git a/controllers/pkg/types/global.go b/controllers/pkg/types/global.go index 12ce3a8db7a1..4404fd102802 100644 --- a/controllers/pkg/types/global.go +++ b/controllers/pkg/types/global.go @@ -301,3 +301,21 @@ type UserRealNameInfo struct { func (UserRealNameInfo) TableName() string { return "UserRealNameInfo" } + +type EnterpriseRealNameInfo struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;unique"` + EnterpriseName *string `gorm:"column:enterpriseName;type:text"` + EnterpriseQualification *string `gorm:"column:enterpriseQualification;type:text"` + LegalRepresentativePhone *string `gorm:"column:legalRepresentativePhone;type:text"` + IsVerified bool `gorm:"column:isVerified;type:boolean;default:false"` + VerificationStatus *string `gorm:"column:verificationStatus;type:text"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp()"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;autoUpdateTime"` + AdditionalInfo json.RawMessage `gorm:"column:additionalInfo;type:jsonb"` + SupportingMaterials json.RawMessage `gorm:"column:supportingMaterials;type:jsonb"` +} + +func (EnterpriseRealNameInfo) TableName() string { + return "EnterpriseRealNameInfo" +} diff --git a/docs/4.0/docs/Community.md b/docs/4.0/docs/Community.md index 6722b4c49c50..2f1b01d67106 100644 --- a/docs/4.0/docs/Community.md +++ b/docs/4.0/docs/Community.md @@ -3,5 +3,5 @@ Sealos is an open source project that is driven by the participation of users and contributors. If you have questions or suggestions about using the product while reading this documentation, please try the following ways to seek support. Our team and community will do their best to help you. + 💬 Join our [Discord server](https://discord.gg/qzBmGGZGk7) is to chat with Sealos developers and other Sealos users. This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences. -+ 🐦 Tweet at @sealosio on [Twitter](https://twitter.com/sealosio) and follow us. ++ 🐦 Tweet at @Sailos_io on [Twitter](https://twitter.com/Sailos_io) and follow us. + 🐞 Create [GitHub Issues](https://github.com/labring/sealos/issues/new/choose) for bug reports and feature requests. \ No newline at end of file diff --git a/docs/4.0/i18n/zh-Hans/Community.md b/docs/4.0/i18n/zh-Hans/Community.md index 1c7654defbdc..c015bb6aee20 100644 --- a/docs/4.0/i18n/zh-Hans/Community.md +++ b/docs/4.0/i18n/zh-Hans/Community.md @@ -7,5 +7,5 @@ Sealos 是一个由用户和贡献者参与推动的开源项目,如果您对 ![](https://oss.laf.run/htr4n1-images/sealos-qr-code.jpg) + 💬 加入我们的 [Discord 服务器](https://discord.gg/qzBmGGZGk7),与 Sealos 开发者和终端用户进行交流。 -+ 🐦 在 [Twitter](https://twitter.com/sealosio) 上关注我们。 ++ 🐦 在 [Twitter](https://twitter.com/Sailos_io) 上关注我们。 + 🐞 请将任何 Sealos 的 Bug、问题和需求提交到 [GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。 \ No newline at end of file diff --git a/docs/5.0/docs/community.md b/docs/5.0/docs/community.md index 696dbfcc7fa1..eb8c107e584c 100644 --- a/docs/5.0/docs/community.md +++ b/docs/5.0/docs/community.md @@ -1,6 +1,6 @@ --- keywords: [Sealos, open source project, Kubernetes, Discord server, GitHub Issues] -description: Sealos is an open source project driven by user participation. Join our Discord server, tweet at @sealosio, or create GitHub Issues for support. +description: Sealos is an open source project driven by user participation. Join our Discord server, tweet at @Sailos_io, or create GitHub Issues for support. --- # Community @@ -11,5 +11,5 @@ team and community will do their best to help you. + 💬 Join our [Discord server](https://discord.gg/qzBmGGZGk7) is to chat with Sealos developers and other Sealos users. This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences. -+ 🐦 Tweet at @sealosio on [Twitter](https://twitter.com/sealosio) and follow us. ++ 🐦 Tweet at @Sailos_io on [Twitter](https://twitter.com/Sailos_io) and follow us. + 🐞 Create [GitHub Issues](https://github.com/labring/sealos/issues/new/choose) for bug reports and feature requests. \ No newline at end of file diff --git a/docs/5.0/docs/user-guide/devbox/faq.md b/docs/5.0/docs/user-guide/devbox/faq.md index 41fb84a6df14..0e55a5164aff 100644 --- a/docs/5.0/docs/user-guide/devbox/faq.md +++ b/docs/5.0/docs/user-guide/devbox/faq.md @@ -114,33 +114,4 @@ Alternatively, you could change your application's port. ## 9. How to Change the Default User to Root in Devbox -If you want to connect to Devbox as the root user when opening a local editor, please follow these steps. First, ensure -your Devbox can connect properly using the default configuration. Open the file `~/.ssh/sealos/devbox_config` on your -local machine. If you are uncertain about the file's location, please refer to Question 2. - -Locate the configuration that you wish to modify to use the root user, for example: - -```config -Host usw.sailos.io_ns-rqtny6y6_devbox1234 - HostName usw.sailos.io - User devbox - Port 40911 - IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox1234 - IdentitiesOnly yes - StrictHostKeyChecking no -``` - -Change the `User` value to root, as follows: - -```config -Host usw.sailos.io_ns-rqtny6y6_devbox1234 - HostName usw.sailos.io - User root - Port 40911 - IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox1234 - IdentitiesOnly yes - StrictHostKeyChecking no -``` - -After saving the changes, exit the local editor connected to Devbox and then reopen the editor. You should now be -connected to Devbox as the root user. \ No newline at end of file +Run 'sudo su' in the terminal to switch to the root user. diff --git a/docs/5.0/i18n/zh-Hans/community.md b/docs/5.0/i18n/zh-Hans/community.md index a7f0dd161af0..274cb076d243 100644 --- a/docs/5.0/i18n/zh-Hans/community.md +++ b/docs/5.0/i18n/zh-Hans/community.md @@ -12,5 +12,5 @@ Sealos 是一个由用户和贡献者参与推动的开源项目,如果您对 ![](https://oss.laf.run/htr4n1-images/sealos-qr-code.jpg) + 💬 加入我们的 [Discord 服务器](https://discord.gg/qzBmGGZGk7),与 Sealos 开发者和终端用户进行交流。 -+ 🐦 在 [Twitter](https://twitter.com/sealosio) 上关注我们。 ++ 🐦 在 [Twitter](https://twitter.com/Sailos_io) 上关注我们。 + 🐞 请将任何 Sealos 的 Bug、问题和需求提交到 [GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。 \ No newline at end of file diff --git a/docs/5.0/i18n/zh-Hans/user-guide/devbox/faq.md b/docs/5.0/i18n/zh-Hans/user-guide/devbox/faq.md index 794a022a44f1..2fdb800ffb9e 100644 --- a/docs/5.0/i18n/zh-Hans/user-guide/devbox/faq.md +++ b/docs/5.0/i18n/zh-Hans/user-guide/devbox/faq.md @@ -107,34 +107,7 @@ sudo go run main.go 或者变更你的端口。 -## 9、如何在 Devbox 中更换默认的用户为 root 用户 - -如果你需要在打开本地编辑器时,链接到 Devbox 的用户为 root,可以参考以下的步骤。 -首先请确保你的 Devbox 在使用默认的配置时,可以正常连接。 -打开本地电脑的 `~/.ssh/sealos/devbox_config`,如果不清楚这个文件在哪里,请参考问题2。 - -找到你需要切换为 root 用户的配置,例如: -```config -Host usw.sailos.io_ns-rqtny6y6_devbox1234 - HostName usw.sailos.io - User devbox - Port 40911 - IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox1234 - IdentitiesOnly yes - StrictHostKeyChecking no -``` - -修改 `User` 的值为 root,例如: - -```config -Host usw.sailos.io_ns-rqtny6y6_devbox1234 - HostName usw.sailos.io - User root - Port 40911 - IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox1234 - IdentitiesOnly yes - StrictHostKeyChecking no -``` +## 9、如何在 Devbox 中更换默认的用户为 root 用户 -保存之后退出打开了 Devbox 的本地编辑器,重新打开编辑器,连接到 Devbox,此时的用户已经切换为 root。 \ No newline at end of file +终端里运行`sudo su`即可切换到 root 用户。 diff --git a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/envoy-resource.png b/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/envoy-resource.png deleted file mode 100644 index c3db14f508dc..000000000000 Binary files a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/envoy-resource.png and /dev/null differ diff --git a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-controller.png b/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-controller.png deleted file mode 100644 index c4835a35efba..000000000000 Binary files a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-controller.png and /dev/null differ diff --git a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-gateway.png b/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-gateway.png deleted file mode 100644 index e607ff6143bc..000000000000 Binary files a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-gateway.png and /dev/null differ diff --git a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/nginx-resource.png b/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/nginx-resource.png deleted file mode 100644 index aa36990d2d9c..000000000000 Binary files a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/nginx-resource.png and /dev/null differ diff --git a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/index.md b/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/index.md deleted file mode 100644 index 11107935c333..000000000000 --- a/docs/blog/en/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/index.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -slug: how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice -title: How Sealos Cloud Mastered Multi-Tenancy with the Right Gateway Choice -description: Explore how Sealos Cloud optimizes its gateway selection amidst handling hundreds of thousands of Ingress rules and strict multi-tenancy requirements. From the limitations of Nginx Ingress to the eventual choice of Higress, this article delves deep into the performance, stability, and security of various gateway options, offering valuable insights for gateway selection in multi-tenant scenarios in public network environments. -authors: [fanux] -tags: [Kubernetes, Sealos, Gateway] -keywords: [Cloud Operating System, Sealos, K8s, Cloud Native, Gateway, Envoy, Ingress] -image: images/feature.jpg -date: 2024-01-20T10:00 ---- - -[Sealos](https://sealos.io) Cloud has stretched the capabilities of nearly all the leading open-source gateways to their limits. This article aims to serve as a practical guide, helping to navigate common challenges and offering advice for choosing the right gateway. - - - -## Complex Challenges in Sealos Cloud - -Ever since the launch of [Sealos Cloud](https://cloud.sealos.io), the platform has seen a meteoric rise in user numbers, currently standing at **87,000 registered users**. Each of these users creates applications, and each application demands its individual access point, leading to an extraordinarily large number of routing entries across the entire cluster. **This necessitates support for ingress capabilities on the scale of hundreds of thousands.** - -Furthermore, offering shared cluster services on the public internet places stringent demands on multi-tenancy. It's crucial that user routes are completely isolated from one another to ensure optimal route integrity, demanding high-quality isolation and sophisticated traffic control measures. - -The exposure to potential cyber threats is extensive in public clouds. Hackers not only target applications running on the cloud but also aim at the platform's outbound network infrastructure, thereby intensifying the security challenges. - -The demands on controller performance and stability are substantial. Many controllers, when faced with an increasing number of routing entries, consume extensive resources, which can sometimes lead to Out of Memory (OOM) issues, ultimately causing gateway failures. - -## Nginx Ingress - -Our initial choice was Nginx Ingress, but we eventually faced several critical issues that proved to be deal-breakers: - -* **Reload dilemma**: Every ingress modification results in brief disconnections. In a cluster bustling with users, the high frequency of ingress adjustments leads to consistent network instability. -* **Unreliable long connections**: Due to the dynamic nature of adjustments, long-term connections are prone to frequent disruptions. -* **Suboptimal performance:** The gateway's responsiveness is sluggish, and it's relatively resource-hungry. - -These significant concerns have led us to move away from gateways rooted in Nginx architecture. Our empirical testing revealed that Envoy-based gateways significantly outperform others, showing minimal performance overhead in both the control and data planes. - -Here's a glimpse of Envoy's performance: - -![](images/envoy-resource.png) - -In contrast, here's what we observed with Nginx's performance: - -![](images/nginx-resource.png) - -The disparity is pronounced, leading us to decisively set aside options based on Nginx and fully embrace the robust capabilities of Envoy. - -## APISIX - -[APISIX](https://github.com/apache/apisix) is a commendable project, particularly in addressing Nginx reload issues. At [Laf](https://github.com/labring/laf), we initially embraced APISIX. However, we encountered instability with its Ingress Controller, leading to frequent major disruptions and controller OOM issues. Despite our preference for APISIX, these persistent issues necessitated a switch to an alternative gateway. The APISIX community is actively working on these challenges, and we look forward to its continued improvement. - -In summary, while APISIX demonstrates excellent stability, its controller still requires significant optimization and stability enhancements. The community provides robust support, but **due to our immediate operational challenges, we had to transition to a different gateway solution.** - -## Cilium Gateway - -Having switched our [CNI](https://sealos.io/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes#install-kubernetes-cluster) to Cilium early on, we recognized its potential and contemplated using the Cilium Gateway. However, reality presented its challenges. - -[Cilium Gateway](https://cilium.io/use-cases/gateway-api/) exclusively supports LB mode, creating a dependency on cloud provider LBs. Given our need for private deployment scenarios, this dependence was undesirable. In terms of stability, the Ingress activation delay in scenarios with numerous routes was significantly prolonged, taking minutes instead of the preferable 5 seconds. **Therefore, we concluded that it's necessary to wait for further development in this aspect.** - -## Envoy Gateway - -In the realm of Kubernetes (K8s) standards, there is a noticeable shift from the traditional Ingress to the Gateway standard. Our foundational preference for Envoy leads us to consider the implementation of the [Envoy Gateway](https://github.com/envoyproxy/gateway) as a promising option. Our exploration of the Envoy Gateway revealed that it is still in a preliminary phase, plagued by several instability issues like memory overflows, path policies not being effective, and certain functionalities not working in the merge gateway mode. We are actively engaged in resolving these issues and are contributing to the upstream community with constructive feedback and improvements. Our aim is to nurture the Envoy Gateway to a level where it becomes fully viable for production environments. - -## The High-Prestige but Less Practical Gateway Standard - -The Gateway standard finds itself in a tricky situation. It appears the designers may not have thoroughly explored multi-tenant environments in practice. When a cluster is shared among multiple tenants, it is crucial to clearly define and separate the rights and responsibilities of administrators and users. Gateway's initial design overlooked this aspect. For instance: - -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: eg -spec: - gatewayClassName: eg - listeners: - - name: http - port: 80 - protocol: HTTP - # hostname: "*.example.com" - - name: https - port: 443 - protocol: HTTPS - # hostname: "*.example.com" - tls: - mode: Terminate - certificateRefs: - - kind: Secret - name: example-com -``` - -Settings like listening ports should ideally be managed by cluster administrators rather than ordinary users. On the other hand, configuring TLS certificates should be more user-centric, although administrators might still need some control. However, in this setup, the delineation of permissions is unclear. Consequently, users are also given the ability to configure the Gateway, leading to the necessity for intricate permission control in the controller, such as managing port whitelists and detecting conflicts. - -A more sophisticated design approach might involve relocating tenant-level fields to HTTPRoute or establishing a separate CRD, thereby clarifying the distinction between regular users and super administrators. The current approach is functional, but it tends to be somewhat muddled and less efficient. - -## Higress: The Clear Winner - -Beyond the key projects we focused on, numerous others were evaluated but are not listed here. Ultimately, Sealos chose [Higress](https://github.com/alibaba/higress) for its gateway needs. - -Our criteria for gateway selection were straightforward: we sought a solution that was not only functionally adequate but also highly stable. Higress emerged as our choice, essentially by process of elimination. - -**Stability was our primary concern. Among the contenders, Higress was the only one meeting our production standards**, although some challenges arose. Thankfully, the proactive Higress community swiftly resolved these issues. Notable challenges included: - -1. **Ingress Activation Speed** – Initially, it took over two minutes for new routes to activate when dealing with many entries. This was optimized by the community to about 3 seconds, a remarkable improvement that eliminated the need for further optimization, as it now outpaces the container Ready time. Higress's use of an incremental configuration loading approach ensures exceptional performance, even with a high volume of routing entries. -2. **Controller OOM** – Previously, the controller faced memory issues due to high resource consumption without dynamic loading. These issues have been effectively addressed. -3. **Timeout Issues** – In one of our primary clusters, we encountered sporadic request timeouts related to the onDemandRDS configuration. We've temporarily disabled this feature and are investigating further. This problem was not present in our other clusters. - -From a security standpoint, many issues we faced were linked to performance bottlenecks, such as traffic surges overwhelming the gateway. This underscores the vital importance of gateway performance. In our tests, Envoy demonstrated remarkable robustness, and the design of the controller proved to be a critical factor in operational success. Higress has shown exceptional performance in this regard: - -![](images/higress-controller.png) - -![](images/higress-gateway.png) - -Given our extensive routing and high-volume traffic, Higress stands out for its remarkably low resource demand. - -Higress also boasts compatibility with Nginx Ingress syntax, mainly in annotations. Our prior reliance on Ingress meant there was almost no need for code migration, allowing for a swift upgrade process within minutes. - -To further encourage community development, we also have recommendations for Higress: - -* Enhanced support for the Gateway standard is needed. Although it already supports the v1 version, it lacks full compatibility with the features available on Ingress. -* We suggest the introduction of more sophisticated functionalities, especially in areas like security and circuit breaking. We are open to paying for these features, but as our platform evolves, stronger functionalities become necessary. -* We advise developing additional peripheral features through a plugin mechanism, aiming to make the core features more cohesive, simpler, and more reliable. - -## Summary - -Gateways are a fundamental component for cloud services and applications. As our scale grows, we face an array of new challenges. Our aim is to build strong collaborations with the broader community, enhancing the development of open-source gateways and benefiting a larger pool of developers. - -The gateways mentioned are all of high quality. Sealos not utilizing them is not a commentary on their effectiveness but a reflection of our unique and stringent scenarios. Gateways that support multi-tenancy in public internet environments are few. Therefore, it's crucial for decision-makers to consider their specific scenarios. Our choices should serve as a guideline, and Sealos is dedicated to maintaining an open approach in monitoring the evolution of various gateways. - -We are immensely grateful to the Higress open-source community for their substantial support and to the Alibaba Cloud Native Team for contributing such a valuable project to the community. diff --git a/docs/blog/en/2023/k8s-multi-tenancy.md b/docs/blog/en/2023/k8s-multi-tenancy.md deleted file mode 100644 index 99239400d712..000000000000 --- a/docs/blog/en/2023/k8s-multi-tenancy.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -slug: k8s-multi-tenancy -title: The Promise and Challenges of Kubernetes Multi-Tenant -description: explores the value proposition of multi-tenant Kubernetes, implementation hurdles, and potential solutions to unlock its benefits. -authors: [fanux] -tags: [Kubernetes, Sealos, Multi-Tenant] -keywords: [Cloud Operating System, Sealos, K8s, Cloud Native, Cloud Computing, Cloud OS, PaaS, Multi-Tenant, Runtime Isolation, Namespace] -image: https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-17-36-fBsk9p.jpg -date: 2023-11-29T10:00 ---- - -In today's business landscape, managing cloud and server resources is becoming increasingly unwieldy as companies diversify and scale. While powerful, Kubernetes lacks native support for multi-tenancy - the ability to securely isolate multiple tenant workloads. This gap creates deployment friction for teams and missed efficiency opportunities for enterprises. - -**This article explores the value proposition of multi-tenant Kubernetes, implementation hurdles, and potential solutions to unlock its benefits.** - - - -## The Potential of Multi-Tenant Kubernetes - -Multi-tenancy refers to an architecture allowing multiple users or "tenants" to share resources from the same system while keeping data isolated and secure. For Kubernetes, this means running workloads from different teams on a shared cluster without risk of resource conflicts, data leaks, or security issues. - -![Diagram of single vs multi-tenant Kubernetes](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-rLPyaY.jpg) - -### Pain Points of Single-Tenant Setups - -Consider an enterprise Kubernetes cluster used by 20 internal departments. Without multi-tenancy, several pain points emerge: - -1. **Inefficiency** - Deployments get bottlenecked through cluster administrators, hampering velocity. -2. **Underutilization** - Workloads cannot mix, leading to resource stranding. -3. **Sprawl** - Lacking isolation allows cluster entanglement over time. -4. **Limitations** - Fixed single-tenant structure strains under changing demands. - -![Comparison table of single vs multi-tenant Kubernetes](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-15-53-DGg4ig.png) - -### The Multi-Tenant Advantage - -Conversely, effective multi-tenancy unlocks greater cloud agility: - -1. **Organization** - Teams self-manage resources without wasteful allocation conflicts. -2. **Velocity** - Services rapidly provision without administrative bottlenecks. -3. **Efficiency** - Cluster administrators focus holistically rather than application-by-application. -4. **Resilience** - Workload isolation enhances stability despite diverse deployments. - -## Navigating Multi-Tenancy Challenges in Kubernetes - -In the Kubernetes (K8s) landscape, establishing a multi-tenant architecture is a complex endeavor, transcending the mere application of namespaces and involving an array of technical intricacies. - -### Challenge 1: Curbing Over-Privilege Risks - -Central to a multi-tenant K8s framework is the strict regulation of user permissions. In scenarios where a cluster is shared among several users, overly privileged individuals can pose a serious threat. Prohibitions against accessing server nodes or executing node-level commands, such as `kubectl get node`, are essential. Further, it's crucial to curtail other high-risk activities, including the activation of container privileged modes, and sharing of host filesystems, ports, and networks. - -Sealos addresses these concerns through a multi-faceted isolation approach. It employs OpenEBS for block-level storage isolation, Firecracker and Cloud Hypervisor for computational isolation, and Cilium for network isolation, ensuring that the activities of one tenant do not adversely affect others. - -### Challenge 2: User Identity, Authorization, and Namespace Ties - -Inherent to K8s is the absence of a native user management framework. This necessitates the creation of a user identity system, integration with external user management platforms, and issuance of unique kubeconfig files or tokens. Moreover, it's imperative to forge a multifaceted linkage between users and namespaces, coupled with the distribution of tailored permissions. - -![Image Depicting User-Namespace Association](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-Dfn5xa.png) - -Sealos's framework enables administrators to effectively slot users into designated namespaces and regulate their roles, thereby achieving a granular control over permissions. This guarantees that users access only the resources they are legitimately permitted to use. - -![Image Illustrating User Permissions Management](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-wknQxI.png) - -![Image Showcasing User Role Control](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-RQFrTB.png) - -### Challenge 3: Metering and Managing Quotas - -A critical aspect of multi-tenancy in K8s is the equitable distribution and meticulous tracking of resource usage, including CPU, memory, disk, and network utilization. Managing excess usage and differentiating between internal and external network traffic are particularly challenging, as is accurately attributing traffic to specific containers and tenants. - -Utilizing eBPF technology, Sealos adeptly monitors network traffic, correlating it with tenant information and storing it in a database for precise billing and resource management. For compute and storage resources, Sealos relies on controllers to gather and administer relevant data, ensuring efficient resource oversight. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-36-HsycaI.png) - -## Extreme Multi-Tenancy - The Sealos Challenge - -In the realm of multi-tenancy, Sealos embarks on an ambitious journey, operating within the unpredictable confines of a public network. This scenario invites any developer to join and partake in a communal Kubernetes cluster, which inherently raises substantial security and stability risks. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-54-kbCMsN.png) - -The method adopted by Sealos brings forth distinct benefits: cost-effectiveness, as it negates the need for users to independently build and manage their clusters, leading to significant cost reductions in cloud services. It also enhances resource utilization, allowing container operations on a smaller scale, thereby leveraging the platform’s flexibility and resources. Crucially, establishing strong isolation in such a public network setting can bolster security and stability. - -Nevertheless, Sealos is confronted with a series of formidable challenges: - -**Challenge 1: Overcoming Gateway Limitations** The substantial user base of Sealos generates immense traffic, pushing the boundaries of many mainstream open-source gateways. A single user update can potentially impact the entire user base, as systems like Nginx require configuration reloads. Moreover, specific well-known gateways, which remain unnamed, struggle with issues like CPU overload and delays in configuration effectiveness. Sealos has proactively engaged with upstream communities for potential improvements. - -**Challenge 2: Addressing Runtime Isolation** Strong isolation is critical for ensuring security in Sealos's multi-tenant environment, but current mainstream runtime environments fall short of meeting these requirements. For example, Firecracker's inadequate GPU support presents a significant limitation for high-performance computing applications. - -**Challenge 3: Ensuring Storage Isolation** A paramount concern for Sealos is the isolation of tenant data to prevent unauthorized access or data breaches. The goal is to implement block-level storage isolation, a challenging but necessary endeavor. - -**Challenge 4: Network Metering and Contention Management** Managing and accurately metering network resources is essential in a multi-tenant infrastructure. Sealos is committed to distributing these resources fairly, particularly in situations where resource contention occurs, to ensure that all users have fair and efficient access to network resources. - -**Summary** - -Only with the advancement and maturity of multi-tenancy can the true essence of a cloud be actualized, harnessing over ninety percent of its inherent capabilities. Facing the complexities and unpredictabilities of the public internet, Sealos excels by providing secure and isolated multi-tenant environments. It achieves this while ensuring efficiency and reducing operational costs. The underlying technology Sealos uses is elegantly designed, catering perfectly to businesses where a single cloud platform is shared among all developers. \ No newline at end of file diff --git a/docs/blog/en/2023/sealos-release.md b/docs/blog/en/2023/sealos-release.md deleted file mode 100644 index 5725dc826a9d..000000000000 --- a/docs/blog/en/2023/sealos-release.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -slug: sealos-release -title: "Sealos: Revolutionizing Cloud Computing with a User-Friendly Operating System" -description: Explore the journey of Sealos, a grand cloud operating system project, from its inception as a simple Kubernetes installer to a comprehensive platform transforming cloud computing. Dive into the creator's story, the evolution of Sealos, and how it simplifies cloud operations for businesses and individuals alike. -authors: [fanux] -tags: [Kubernetes, Sealos] -keywords: [cloud operating system, Sealos, Kubernetes, cloud native, Cloud computing, cluster image, Sealer, cloud-native technologies] -image: https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting5@main/uPic/2023-08-31-09-52-gLmSek.jpg -date: 2023-06-13T10:00 ---- - -In the ever-evolving landscape of cloud computing, a groundbreaking innovation has emerged, reshaping our understanding of cloud infrastructures. Sealos, transcending its initial scope as a Kubernetes installation tool, has emerged as a pioneering [cloud operating system](https://sealos.io). In this blog, we'll delve into the journey of Sealos, from its inception to becoming a cornerstone in cloud computing, offering unparalleled user experience and efficiency. - - - -## The Inception of Sealos - -It all started one quiet night in 2018. A single line of code marked the beginning of something extraordinary. Initially named "kubeinit," the project soon outgrew its initial scope. Recognizing the need for a broader vision, the project was aptly renamed Sealos. It was not merely about installing Kubernetes; it was about piecing together a comprehensive [cloud operating system](https://sealos.io). This pivotal moment set the stage for the evolution of Sealos, a vision that would redefine cloud computing. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-08-21-15-15-rcAujq.webp) - -## The Entrepreneurial Journey and Initial Challenges - -The journey of Sealos is a tale of challenges, perseverance, and innovation. When Sealos made its debut on the Alibaba Cloud Marketplace at 15 yuan per copy, the expectation of commercial success was modest. But the first sale, a mere 15 yuan, was a monumental milestone. It was more than just a transaction; it was a validation of potential, a glimpse into a future of endless possibilities. - -However, this initial success came with its own set of challenges. Providing after-sales service proved to be a Herculean task, demanding constant attention and problem-solving, often at the cost of personal time. This phase was crucial, laying the groundwork for understanding user needs and driving Sealos towards excellence. - -## Enhancements and Innovations in Sealos - -The development journey of Sealos has been marked by constant innovation and improvement. The initial version, while functional, was just the beginning. Recognizing the need for a more robust solution, Sealos evolved into its second iteration, leveraging Ansible for enhanced performance. - -However, the pursuit of perfection never ceased. The third version of Sealos was a breakthrough, simplifying load balancing and eliminating dependencies, making it a pinnacle in installation simplicity. This relentless pursuit of innovation reflects the core ethos of Sealos – to simplify complex cloud operations while enhancing efficiency and reliability. - -## Focus on Installation: A Strategic Choice - -Installation is often the first encounter users have with any software, and for Sealos, it was crucial to make this experience as smooth as possible. Focusing on installation was a strategic choice, ensuring that users could easily step into the world of cloud-native technologies. This focus also set the stage for users to explore the full spectrum of Sealos' capabilities, fostering a deeper engagement with the platform. - -## Sealos at Alibaba: The Birth of Sealer - -During its tenure at Alibaba, Sealos underwent a significant transformation with the development of Sealer. This was a pivotal point, as Sealer brought unparalleled flexibility to the installation process. The concept of cluster imaging, akin to a "cloud version Docker image," was introduced, allowing users to define their installation packages. This innovation not only enhanced the abstraction levels but also offered unprecedented flexibility, reinforcing Sealos' position as a frontrunner in [cloud operating system](https://sealos.io)s. - -``` -dockerfile -FROM kubernetes:v1.25.0 -COPY mysql . -CMD helm install mysql . -``` - -This concept allows the [cloud operating system](https://sealos.io) to have "images" just like a standalone operating system. Another step is completed in this grand vision. - -## Sealos as a [cloud operating system](https://sealos.io) - -At its core, Sealos is more than just a set of tools; it's a full-fledged [cloud operating system](https://sealos.io). With the mantra "Everything is an application," Sealos redefines how we perceive data centers and cloud resources. By treating the entire data center as a unified entity rather than isolated servers, Sealos turns it into a virtual supercomputer, offering seamless, distributed application management. This approach not only simplifies the cloud computing process but also maximizes efficiency and scalability. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-08-21-15-30-Zn4l1W.webp) - -## Design Philosophy: Minimalism and User-Centric Approach - -In a domain often cluttered with complexity, Sealos stands out with its minimalist design. But don't be fooled by its simplicity; every element of Sealos is designed with powerful functionality in mind. This design philosophy stems from a deep understanding of user needs, especially in the B2B software realm where user experience is often overlooked. Sealos breaks this norm, ensuring that every interaction with the platform is intuitive, efficient, and pleasant. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-08-21-15-31-jqkByJ.png) - -The black, white, and gray design style will make you feel like you're drinking plain water while using the product, rather than a beverage, let alone footwash (as some products make you feel like dying). Developers already suffer enough, and I hope that using Sealos will bring you a pleasant mood. - -Sealos can pinpoint the pain points of applications. For example, the App Launchpad, an application manager, allows you to launch your own application within 30 seconds. This involves numerous details, such as automatically configuring public domain names and resolving HTTPS certificate issues. - -## Affordability and Efficiency with Sealos - -One of the most compelling aspects of Sealos is its cost-effectiveness. The platform allows for the efficient running of applications, significantly reducing operational costs. This affordability is achieved through innovative approaches like paying only for active containers and automatic scaling during low traffic periods. Such features make Sealos not just a powerful [cloud operating system](https://sealos.io), but also an economically viable solution for businesses of all sizes. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-08-21-15-34-NmR7oB.png) - -For enterprises, this can significantly reduce resource utilization costs. We ourselves run over 7,000 applications on just 10 servers. What does that mean? After deploying a Sealos cluster, as long as the server resource utilization is below 70%, you can continuously add applications to the cluster until it reaches its capacity. - -You might wonder, **why not use Kubernetes directly?** The reason is simple. For enterprises like Xunfei, applications are distributed across various departments, making multi-tenancy, isolation, and collaboration crucial. Using Kubernetes directly could disrupt the cluster, and the worst-case scenario is that a department or user inadvertently causes a security issue that crashes the entire cluster. Sealos perfectly solves this problem! - -Sealos can help 80% of enterprises reduce their resource utilization costs by 80%. - -## Sealos: Liberating Cloud Management - -Sealos champions the principle of "everything is an application," catering to a diverse range of users, from novices to cloud-native experts. This design ensures that users can leverage Sealos without the burden of understanding complex Kubernetes concepts. At the same time, it provides flexibility and power to those who are well-versed in cloud-native technologies. This dual approach democratizes cloud management, making it accessible to a broader audience while retaining the depth and flexibility for experts. - -Sealos pays great attention to the coordination between applications. For example, if you're using function compute on Sealos, the default database might be MongoDB. But what if you want to use PostgreSQL? In this case, you can install a PostgreSQL application on Sealos and access it directly within the function compute through service discovery. Since they are in the same cluster, they can directly communicate through internal DNS. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-08-21-15-37-5VOWuC.png) - -Sealos is streamlined yet not simplistic. All components can be uninstalled, allowing the cloud to perfectly meet your needs—more is considered excessive, less is considered insufficient. This also means that whether it's a single server or hundreds of data centers, they can be built into a cloud with just one command. - -## Real-World Applications and User Base - -- Run an nginx demo on Sealos in just 30 seconds with automatic scaling. -- Start various databases in 30 seconds and connect to them directly within your business system's intranet. -- Launch your business applications written in various programming languages directly on Sealos. - -These three capabilities serve as the foundation, and you can gradually explore and discover new territories. - -When it comes to running your own business, we have made many detailed optimizations for this scenario. For example, automatic allocation of subdomains, horizontal autoscaling, and support for running various stateful services. - -You will find that with Sealos, **whether you're deploying a monitoring system or running a low-code platform, it's all within reach. You can easily host your blog on Sealos at a low cost. Using the Sealos terminal, you can run any Kubernetes-compatible application without difficulty in automation.** - -Sealos' versatility is demonstrated through its wide array of applications across various sectors. From supporting high-concurrency services during critical times to running large-scale GPU clusters, Sealos proves its mettle in the most demanding scenarios. Its growing community, with over 100,000 users, including large enterprises, is a testament to its reliability and efficiency in real-world applications. - -![Sealos Community](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-19-51-Ve2aWX.png) - -## The Future Roadmap of Sealos - -Looking ahead, Sealos is poised for even greater achievements. The roadmap envisions a ubiquitous [cloud operating system](https://sealos.io), offering an experience as seamless and user-friendly as a personal computer. With a commitment to continuous innovation, Sealos aims to enable rapid deployment of new businesses, significant cost reductions, and the simplicity of creating a cloud with just a click. - -The Sealos [cloud operating system](https://sealos.io) will also incorporate a **Copilot**, acting as a navigator's assistant. It can automatically perform cloud-native transformations, helping developers easily enter the realm of cloud-native. It can also assist in diagnosing cluster issues, identifying security vulnerabilities, and providing professional operational advice like an expert. - -## Conclusion - -Reflecting on the five-year journey of Sealos, it's evident that the vision conceived at the first line of code has been realized. This journey, marked by milestones, challenges, and triumphs, was made possible by the trust and support of the community, contributors, and partners. As Sealos embarks on its next phase, it stands ready to exceed expectations and redefine the cloud computing experience. - -Experience the transformative power of the [Sealos cloud operating system](https://cloud.sealos.io). Join our community and embark on a journey of simplified, efficient, and innovative cloud computing. \ No newline at end of file diff --git a/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/feature.jpg b/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/feature.jpg deleted file mode 100644 index 47e90ce07285..000000000000 Binary files a/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/feature.jpg and /dev/null differ diff --git a/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/sealos-database.png b/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/sealos-database.png deleted file mode 100644 index f51861d140ca..000000000000 Binary files a/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/sealos-database.png and /dev/null differ diff --git a/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/index.md b/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/index.md deleted file mode 100644 index e605d45b8b4d..000000000000 --- a/docs/blog/en/2023/to-run-or-not-to-run-a-database-on-kubernetes/index.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -slug: to-run-or-not-to-run-a-database-on-kubernetes -title: To run or not to run a database on Kubernetes? -description: This article thoroughly examines the advantages of using Kubernetes (K8s) for database management, including enhanced stability, performance, and operational efficiency. -authors: [fanux] -tags: [Kubernetes, Sealos, Database] -keywords: [Cloud Operating System, Sealos, K8s, Cloud Native, Cloud Computing, Cloud OS, PaaS, Database, Container] -image: ./images/feature.jpg -date: 2023-12-06T10:00 ---- - -Yesterday, an insightful article by Mr. Feng explored [the drawbacks of deploying databases in K8S](https://mp.weixin.qq.com/s/4a8Qy4O80xqsnytC4l9lRg). - -Debating the merits of containerized databases might have been relevant four years ago, but in 2023, it's crucial to understand the broader acceptance of this technology. - - - -I began working with K8s from its 0.9 version. Initially, with the CSI still in its nascent stage, true stability wasn't achieved until version 1.0. During my time at iFLYTEK, I played a key role in developing and maintaining a comprehensive system that was integral to the company's internal PaaS services. - -We set up a cluster with 30 physical machines, deceptively small yet technologically advanced, running around 3000 applications of varied types including microservices, databases, message queues, and caches. **This cluster, utilized by hundreds of our developers, was maintained with less than half a person's effort, illustrating the efficiency brought by K8s.** - -Additionally, we managed to upgrade the Linux kernel seamlessly, a task unimaginable without the support of K8s. Normally, coordinating such an upgrade could take as long as six months. - -I've also seen a cluster hosting 400 databases, which required 400 servers and a 40-person team, yet operated at less than 10% efficiency. This cluster, a victim of excessive manual maintenance, reflects a common challenge many teams encounter in managing and optimizing their infrastructure efficiently. - -Upon joining Alibaba, I observed that all databases for delivery purposes were operated on K8s. For over five years now, we've been running databases in containers flawlessly, with no incidents. - -## Democratizing Database Expertise on K8s - -In the business sector, companies often confront two primary challenges with database management: either their management skills are not robust enough to maximize database potential, or they face significant expenditures in managing their databases. The concept of "[Database on K8s](https://sealos.io/docs/guides/dbprovider/)" introduces a standardization that addresses these issues. Such standardization facilitates collaboration and alters the dynamics of productivity and production relationships, leading to a substantial increase in efficiency. This approach empowers teams, even those lacking in specialized skills, to leverage professional capabilities. This is analogous to the distinct roles in agriculture and animal husbandry, where focus in their respective domains enhances overall efficiency and output. - -The KubeBlocks team is a prime example of this. Their expertise and accumulation of knowledge in database management are likely superior to that of most companies. They have transformed their experiential knowledge into code, crafting controllers that enable other businesses to operate in an exceptionally streamlined manner. K8s plays a crucial role in making this feasible. - -One common query is: Why not opt for Ansible? Operational staff often favor Ansible due to its compatibility with their tools. However, Ansible is primarily focused on deployment and operational tasks. K8s controllers, on the other hand, are built on the principle that **tasks achievable by machines should not be manually performed by humans**. They facilitate **a constant, 24-hour synchronization between desired and actual states**, a challenging task with Ansible. Would Ansible be the choice for setting up a routine task? - -This mirrors the era before operating systems when programmers manually punched holes in paper tapes to execute programs. Running programs on paper tapes or CDs was possible, but it begged the question of the necessity of operating systems. - -The underlying principle is the same: Ansible is a valuable tool for operational staff, but K8s's objective is to do away with lower-level operational work (such as writing and executing Ansible scripts). K8s enables more efficient and automated database management, granting teams without extensive database management expertise access to high-level services. - -## The Benefits of Running Databases on Kubernetes - -Most concerns around database-on-Kubernetes boil down to: - -**How stable is it?** - -**Can I effectively troubleshoot issues?** - -**Does performance suffice?** - -### Complexity - -Complexity for databases on Kubernetes involves two key aspects: - -1. Build complexity -2. Usage complexity - -**First: Build Complexity** - -Building a production-grade database platform directly atop native Kubernetes proves costly, unfriendly for beginners lacking depth of expertise. You'd need to create multiple critical components yourself - Kubernetes storage drivers, database controllers, etc. Hands-on expertise makes this possible, but proves challenging. - -Hence the appeal of distributions, much like CentOS, Ubuntu, etc. for Linux instead of direct kernel wrangling. Consider Kubernetes the "cloud kernel." Using the raw kernel sans customization leaves an insufficiently robust, user-friendly experience. The kernel merely provides a framework; users must architect and integrate many optimizations themselves. Kubernetes distributions help solve this. For instance, [Sealos delivers](https://sealos.io/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes/) **full systems including highly available clusters, finely-tuned storage, and optimized databases in one click**. Two simple commands: - -```bash -$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 \ ---masters 192.168.64.2,192.168.64.22,192.168.64.20 \ ---nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] - -$ sealos run labring/openebs:v3.9.0 labring/mysql:8.0 -``` - -Done. The complete system with HA cluster, finely-tuned storage, and optimized databases emerges in minutes. While Ansible aids installs, **it cannot handle critical runtime needs like self-healing and multi-tenancy** - key advantages Kubernetes provides for databases-as-a-service. - -**Second: Usage Complexity** - -Leveraging cloud operating system distributions and controllers, users can deploy standardized database services, moving away from script-based solutions. - -![](images/sealos-database.png) - -This interface is designed for universal ease of use. Even beginners can manage to establish a three-replica PostgreSQL cluster, incorporating backup, recovery, and monitoring functions. This capability democratizes access for all developers within an organization and **underscores the fundamental divergence between 'cloud computing thinking' and 'script-based thinking'. Cloud computing democratizes service provision (as a Service), in contrast to the traditional script-based methods which serve more as operational conveniences.** - -### Stability - -Our team, despite not specializing in database technology, has successfully established a highly stable database system. This demonstrates the potential of what dedicated experts in the field can accomplish. For users, this means that database stability concerns can be confidently entrusted to seasoned professionals. - -Consider the [Sealos Public Cloud](https://cloud.sealos.io) as an example. It currently supports thousands of applications with fully containerized databases, all under the maintenance of the KubeBlocks team. Any arising database issues are efficiently handled by them. From a cost-benefit perspective, utilizing KubeBlocks' commercial services is more economical than employing a full-time database administrator. Furthermore, as Sealos architects, we ensure that database users face minimal operational concerns. Our stability standards exceed those of many non-specialist teams. - -Additionally, database lifecycle management involves specific tasks, and over time, stability issues are progressively resolved. These improvements, often made at the code level, incrementally reduce end-user concerns. This parallels the enhanced stability of the Linux system, achieved through ongoing technological development and optimization. **A well-designed software architecture not only improves but also consolidates its robustness, reducing dependence on human oversight. An illustrative comparison is that Oracle users might find themselves enjoying more leisure time than those using open-source MySQL.** - -Hence, both empirical evidence and theoretical considerations suggest that stability should not pose a hurdle for running databases on K8s. **Choosing K8s for database management effectively means leveraging the pooled expertise of numerous preeminent database experts. Their extensive knowledge and skill, embedded in the code, provide standardized, high-quality services to users. Such depth and efficiency of expertise are challenging to replicate with basic scripting alone.** - -### Performance - -It's a common belief that databases in containers underperform, but this is usually due to a lack of proper handling. The KubeBlocks team's in-depth testing and optimization, detailed in their analysis, show that these complexities are not for users to worry about. **The intricacies are already incorporated into the controller's code, making the process user-friendly**. In fact, the effect of containers on database performance is minor, with disk I/O and network bandwidth latency being the real influential factors. - -The OpenEBS raw disk plus database controller solution addresses these performance issues effectively. By employing a database controller, dependency on distributed storage is removed. This controller ensures both high performance and availability for database replicas, irrespective of the service type, and is seamless for the user. In case of a failure, it automatically makes adjustments, offering a superior database user experience. - -[Sealos](https://sealos.io) is a prime example of this solution in action, achieving high availability without compromising performance. It interacts directly with raw disks, facilitating automatic scaling, backup, and recovery. In the event of a node failure, the controller swiftly launches a new node, synchronizes the data, and integrates it into the cluster. These advanced capabilities, beyond the reach of traditional scripting methods that often require manual intervention, showcase the superiority of cloud operating systems. - -Thus, **running databases on K8s not only avoids performance issues but also offers stability that often surpasses the capabilities of many IT operations teams**. This approach is user-friendly and straightforward, with self-service functionalities. Would this be an option you'd consider? - - -## Do not deny or affirm without considering practical scenarios - -In considering whether to containerize databases, we must take into account diverse real-world applications. - -For some companies with stable non-containerized databases and sufficient funds for database experts, there's little motivation to migrate to Kubernetes (K8s). Why risk migration issues? For example, banks often rely on specialized Oracle machines with straightforward subscription models, providing little impetus for change. - -Conversely, many business development teams and organizations now face a compelling choice: **to access sophisticated database capabilities at a minimal cost, thereby focusing their primary efforts on business development.** - -They might choose cloud database services like RDS (Relational Database Service) or Kubernetes (K8s) based database solutions. This method requires an ongoing management process, replacing manual roles and equipping teams with limited database knowledge. This represents a significant trend, where upfront costs (like developing controllers) increase, but the incremental costs for each team utilizing the database substantially decrease. - -There are various approaches to achieve this, such as virtual machines or Ansible, but Kubernetes-based controllers stand out as the superior solution. Even for services offering RDS-like functionalities, the Kubernetes tech stack emerges as the optimal choice. Virtual machines, being more cumbersome and costly, incur greater performance overheads. And for tools like Ansible to achieve self-service and multi-tenancy seems overly optimistic. - -## Summary - -### Kubernetes' Unique Strategic Significance - -Kubernetes brings tremendous force multiplication, like mastering lifelong martial arts training. Without Kubernetes, you might exert 10% of a database's true potential. Used skillfully, Kubernetes massively amplifies operational database efficiency, savings, and resilience. - -### Technological Progress Reshapes Work Patterns - -As technology advances, specialized database users and caretakers decouple. Manual upkeep yields to intelligent automation. Amidst this shift, standardization enables effective collaboration at global scale. No stronger de facto standard for cloud-native automation exists now than containers and Kubernetes, so database adoption seems inevitable. - -### Practical Proof Points - -Globally, many teams have successfully run databases on Kubernetes in production across critical dimensions like cost, usability, stability, performance, and more - with remarkable measurable results. Once accustomed to Kubernetes' advantages, tough to justify reverting to old-school manual operations. For instance, Sealos' architecture evolved from Ansible (v2) to Golang (v3) - now at [v4 and v5](https://github.com/labring/sealos). This exemplifies "cloud native thinking", not legacy "ops script thinking" tied to specific individuals. If a solution lacks even basic APIs, how can we discuss advanced scalability and productivity? Systems should consider machine consumers first, then human ones - this unlocks true automation leverage. Hence, API > CLI > GUI. - -### Operations Roles Transform - -Many legacy DBAs naturally feel inclined to spread Kubernetes database FUD, protecting their niche skillsets. But enlightened technology leadership will consistently discover immense TCO and productivity gains from thoughtful, staged standardization - we've erased entire 40-person ops teams when strategically migrating systems to Kubernetes without layoffs. Without question, many ops engineers justifiably feel their roles threatened amidst displacement by increasingly automated toolchains. But technologies only accelerate; they cannot unwind. Leaders must shepherd transitions. - -### Kubernetes Maturity and Ecosystem Growth - -Kubernetes rapidly matures while its ecosystem explodes with new solutions. Inevitable chaos results as practices and culture race to catch adoption. But time cures challenges - take heart knowing robust distributions have repeatedly emerged across domains like Linux. These tame entropy and ease adoption by curating “batteries-included” solutions optimized for regular challenges. Sealos leads here as a purpose-built Kubernetes cloud-native OS distribution for standardizing database (and other) operational burdens. Recently, across 200+ Sealos users, literally none reported fundamental database operational struggles. A few cited early instability, but root causes like resource misconfigurations got systematically addressed - now they report Kubernetes-automated databases proving >9 times more stable than DIY trial-and-error. - -### The Enterprise Strategic Choice - -Actual organizational needs should determine if and how organizations adopt Kubernetes databases-as-a-service. But the intelligent path brings tremendous advantages: solutions like Sealos + KubeBlocks equate to: - -1. 8+ years of specialized Kubernetes platform expertise -2. A top-tier database team including multiple senior engineers -3. Extreme usability, resilience, and performance - -...all for less than the cost of staffing database specialists internally. Internal political hesitations naturally arise amid shifts. But pragmatic facts speak volumes about the optimal way forward. - -### Final Thoughts - -Rather than rebutting each counterargument exhaustively, I'll leave readers to independently judge the landscape and merits based on results. Please share your perspectives so together we uncover greater collective truth. The future looks abundantly bright for Kubernetes' democratization of reliability and scale across critical domains like databases. \ No newline at end of file diff --git a/docs/blog/en/2023/what-is-sealos.md b/docs/blog/en/2023/what-is-sealos.md deleted file mode 100644 index e45fdafa791d..000000000000 --- a/docs/blog/en/2023/what-is-sealos.md +++ /dev/null @@ -1,337 +0,0 @@ ---- -slug: what-is-sealos -title: "Sealos vs Traditional Cloud Platforms: A Comparative Analysis of Efficiency and Cost" -description: "Explore the innovative Sealos cloud operating system: a Kubernetes-based platform revolutionizing cloud computing. Dive into its design, unique features, and competitive advantages over traditional cloud services. Learn how Sealos optimizes user experience, reduces costs, and simplifies cloud resource management for businesses and developers." -authors: [fanux] -tags: [Kubernetes, Sealos] -keywords: [Cloud Operating System, Sealos, K8s, Cloud Native, Cloud Computing, Cloud OS, PaaS, Rancher, KubeSphere, Cloud Service] -image: https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting5@main/uPic/2023-08-31-09-52-gLmSek.jpg -date: 2023-07-10T10:00 ---- - -With the rapid development and widespread application of cloud computing, businesses and developers are increasingly seeking flexible and efficient ways to manage and deploy cloud resources. In this context, Sealos emerges as not only a [cloud operating system](https://sealos.io) with Kubernetes at its core, but also as an innovative solution aimed at simplifying and optimizing the cloud computing experience. - -This article delves into the core functions, technical features, design philosophy of Sealos, and how it revolutionizes the [cloud operating system](https://sealos.io) domain. We will also explore Sealos' applications in various usage scenarios and compare it with other cloud service platforms in the market to demonstrate its unique competitive advantages and potential. - - - -## What is Sealos? - -Conceptually, Sealos is similar to operating systems like Windows, but with two key differences. First, Sealos does not operate on a single server; its core concept is to **treat the entire data center or resources across multiple servers as a unified whole**. This approach breaks the limitation of traditional operating systems that operate on a single machine, extending resource and application management to a larger scale, **enabling the operation and management of applications across the entire data center**, significantly enhancing the utilization efficiency of cloud resources and operational capabilities. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-13-46-9Nel1a.png) - -Unlike ordinary operating systems that support daily applications like QQ and WeChat, Sealos focuses on providing developers with the environment they need for distributed applications. In the world of Sealos, **complex cloud computing tasks become as simple and intuitive as using a personal computer**. Whether running common web services like Nginx or deploying and managing distributed applications written in various programming languages, Sealos can do it all with one click, greatly reducing the complexity of configuration and management. Its design philosophy emphasizes user-friendliness and simplicity, striving to eliminate technical barriers in using cloud services, making cloud computing's powerful capabilities easily accessible to every user. - -## Core Problems Solved by Sealos - -Sealos primarily addresses the following core issues: - -### Optimizing Cloud Experience - -#### User Interface - -The significance of the User Interface (UI) is self-evident. Traditional standalone operating systems have provided us with a standardized user experience paradigm. However, many of today’s cloud platforms have evolved into vast and complex systems, causing users to lose their way in the product. This has even led to the creation of specialized training positions for cloud services, reflecting a failure in product design to some extent. For example, few people need training to use an iPhone, as its product design is already excellent and very easy to understand and operate. - -In product design philosophy, we first need to recognize that different user roles have varying focal points. The user base of cloud services includes developers, database administrators (DBAs), operations personnel, Kubernetes (k8s) experts, technical novices, and industry experts. Trying to satisfy all these roles with one product is nearly impossible. For instance, even with the same CI/CD (Continuous Integration/Continuous Deployment) tool, some prefer Jenkins, while others favor Drone. - -Many PaaS platforms take CI/CD as an example and often integrate specific tools like Jenkins, leading to a need to rebuild core functions when the tool becomes less popular or better alternatives emerge. Also, this design fails to meet the preferences of different users. - -The approach of standalone operating systems is worth learning from. The operating system itself does not interfere too much but is responsible for managing applications well. This allows users to freely choose their preferred applications, such as office software DingTalk or Feishu, which are independent of the Windows operating system. This method grants users a great degree of freedom. Although many PaaS platforms also offer app markets, they do not consider applications as the primary element. Instead, most platforms focus on Kubernetes as the core, which is not wrong per se, but **this approach only targets the cloud-native user group and fails to achieve a high level of abstraction**. - -The Sealos platform fully follows the operating system philosophy. It focuses on the specific functions users need. For example, a DBA creating a database does not need to worry about the details of Kubernetes; users of Function Compute services do not need to be concerned about whether they run in containers; and Kubernetes experts can operate through the Lens application or command-line tools. This design philosophy allows various types of users to find suitable tools and services on its platform, thereby optimizing the overall user experience. - -#### API > CLI > GUI - -Many people's understanding of a product is limited to its GUI, but in reality, a cloud service product without an API is almost useless to businesses. To improve efficiency, businesses need to connect and integrate various systems, highlighting the importance of APIs. Cloud services are often designed not just for human users but also for other programs or systems, to achieve a high degree of automation in enterprise operations. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-14-36-qnQmDa.jpg) - -Sealos provides APIs that are fully compatible with Kubernetes' CRD (Custom Resource Definitions) design. Users can manage and control their cloud resources through Sealos' APIs in the same way as operating in a Kubernetes environment. For security, Sealos assigns a permission-limited kubeconfig authentication file to each tenant. These files allow tenants to flexibly connect, manage, and secure different systems and resources. - -This design not only makes Sealos' cloud services more powerful and flexible but also provides enterprises with an efficient, automated way to manage their cloud infrastructure. With the extensive application of APIs, businesses can easily integrate Sealos cloud services into their existing workflows, thus enhancing operational efficiency and flexibility. - -#### Fast and Efficient Operation Experience - -Our goal is to ensure that most operations can be completed within 30 seconds, and at most not exceed 3 minutes. If the operation time of a feature exceeds this standard, there must be a problem, necessitating reevaluation and redesign. - -#### Catering to All Users - -Although Sealos is primarily aimed at developers, in the process of functional design, we also pay attention to the experience of non-technical background users. For this purpose, we specifically invite administrative staff without technical backgrounds to experience our services firsthand. This helps us validate the ease of use of our product. If they can smoothly complete the operation process, it proves that our product is easy to operate and user-friendly. The ease of use of the product is our core pursuit. If users need guidance from others to use the service, it indicates that our design still has room for improvement. - -#### Focus on High-Quality Applications - -In the field of application development, Sealos always prioritizes quality over quantity. For the vast majority of applications, a stable operating environment and backend database support are indispensable. We focus on refining these basic applications first, and then expand to other application fields, integrating cutting-edge applications from various directions and domains, to provide users with comprehensive and efficient solutions. - -#### Ensuring Scalability and Security - -Sealos does not set standards on its own but strictly follows mature systems and de facto standards, ensuring high compatibility with the entire cloud-native ecosystem. All cloud-native applications can run safely on Sealos, and even some non-productized applications can be run through Sealos' terminal. Our compatibility is built on full support for Kubernetes, with enhanced security measures. - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-14-48-vbJezM.png) - -In Sealos, to prevent improper operations or inappropriate image downloads from causing catastrophic effects on the entire system, each user's permissions are restricted within their own namespace. This permission management mechanism strengthens the security and stability of enterprise-level [cloud operating system](https://sealos.io)s. - -### Reducing Cloud Costs - -Our goal is not just to help you cut costs by 30%—such a target lacks challenge and is too mundane. We aim to minimize the marginal cost of cloud services, ideally to one-tenth of the original cost. How to achieve this? This is the direction we are pursuing. So, how can we achieve this goal? - -#### Redefining Cloud Architecture: Abandoning Traditional Models - -**First, we must abandon the traditional three-tier architecture model of IaaS, PaaS, and SaaS.** - -Why give up this classic architecture? The reason is that the traditional layered model no longer meets the current technological developments and market demands. Take IaaS as an example; it simulates hardware like routers, switches, and virtual machines in data centers through software. While this improves scheduling flexibility, it also leads to a sharp increase in software costs. For instance, OpenStack requires a team of dozens of people to maintain its stability, directly resulting in high software costs. In the past, this approach seemed necessary to improve resource utilization, but now, from an application perspective, many applications don't care if they run in a separate VPC during operation. - -![](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-15-00-2jC2C8.png) - -The above is a diagram I drew five years ago, which is gradually becoming a reality. This is similar to the development history of single-machine operating systems: initially layered, later evolving into a more efficient kernel architecture. The layered architecture of cloud computing also carries historical baggage. Once enterprises abandon IaaS, they can save substantial costs and enjoy higher performance. - -From this new perspective, we find that IaaS is actually unnecessary. Technically speaking, PaaS and SaaS are essentially the same; they are both application-level services and thus do not need to be overly differentiated. In the new cloud kernel architecture, we only need to effectively implement isolation between tenants. This does not require complex and heavyweight solutions. For example, Sealos offers a way to share a Kubernetes cluster among multiple tenants in an untrusted public network environment. We achieve this goal using strong isolation containers (like Firecracker), network policies (like Cilium), and block device isolation for storage (like OpenEBS), not only reducing costs but also achieving better results. - -![](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-15-02-6N4ygp.png) - -#### Increasing Application Density and Scheduling Efficiency - -Unless it's a very heavy compute-intensive application, it's almost embarrassing to not run over 100 applications on a server. On Sealos, we run 800 applications on one server while ensuring application stability. - -This is very important for enterprises, as it significantly reduces the need for hardware resources. If you are a corporate executive, you might want to re-examine your company's overall resource utilization rate, which is often below 20%. Through our method, there is much more room for improvement. - -With Sealos, enterprises can save up to half the cost in a simpler way. - -#### Full Elasticity - -Inactive applications at night should rest and leave resources for offline computing or training tasks. This is actually more advantageous with public clouds, as resources can be directly released, saving a lot of costs. - -Sealos directly incorporates this key feature. If all enterprise applications operate in this way, huge cost savings can be achieved. - -#### Eliminating Zombie Applications and Servers - -There are many development and testing programs in enterprises. How do you know which ones are not being used? There are also many zombie "servers." Some companies can only maintain who used what with an Excel sheet, asking around periodically to retire servers that no one claims. Slightly more advanced ones might use an outdated CMDB. - -The radical solution to this problem is: charging money. Yes, internal enterprise applications should also be charged, and those that owe fees should be shut down directly. - -This way, each department can apply for a budget, and developers apply for a budget. Once the budget is used up, the application is shut down, ensuring no zombie applications in the long term. Sealos brings all servers under unified management, turning the entire cluster into a large resource pool, eliminating the possibility of zombie servers. It also saves enterprises a lot of operational manpower. - -To eliminate resource waste, such fine-tuned operations are needed, and Sealos achieves this purpose at a very low cost. The only thing enterprise managers need to do is allocate money to each sub-account. - -![](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-18-ijXZ4y.png) - -This way, you can precisely control how much each department and each person spends, further analyzing ROI. - -#### Operating the Entire Cloud with Half a Person - -A team of operators? An operations team for each business unit? Have you ever seen Microsoft sell a PC and then provide you with an operator? Therefore, it's still about inadequate software. If the software is sufficiently excellent and stable, there's no need for the role of an operator. Or this role will change, like writing orchestration files or operators. - -The developers of Sealos spent less than half the effort to maintain the entire cloud. Whether it's 8,000, 80,000, 800,000, or 8 million applications, it only requires half a person. This is the [cloud operating system](https://sealos.io), which doesn't increase operational complexity as the scale grows. - -I am a neutral person, but I have an extreme view: in a sufficiently mature cloud, there should be no role for operations. If your enterprise has more than 3 operators (excluding those who move servers), you should seriously reflect on this. - -Here's a clear path for current operations staff: develop [cloud operating system](https://sealos.io)s. - -#### Research and Development Human Resource Costs - -I am a developer, and I spend at least 50% of my effort on things other than development. Those miscellaneous tasks may account for 20%, but their impact is 80%. They interrupt what I'm doing, like when you finish writing code and think about selling servers, configuring certificates, packaging, and going live. I bet no developer likes doing these things unless they're masochistic. Developers are lazy, and to be lazy, they develop a bunch of tools. This is the victory of the lazy, and Sealos is also created by the lazy. So if something can be automated, never do it manually; if AI can do it, never do it manually. - -![](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-18-as6HSI.png) - -Analyzing issues yourself is tiring; AI is more professional than humans. - -The function computing capability of Laf on Sealos makes writing code as simple as writing a blog post. Just click save, shut down, and leave. To leave work early, use Laf efficiently. - -All backend dependencies, like databases, can be resolved in under 30 seconds. In the future, AI will automatically package, deploy, code, and debug. - -This intangible increase in development efficiency can save unimaginable costs. In our customer examples, there are many cases of two people doing the work of five. - -### One-Click Private Cloud Construction, Consistent Experience Between Public and Private Clouds - -Sealos has a profound understanding of cloud computing: - -**Public and private clouds are the same thing, the same abstraction, the same set of codes, and the same experience, just with different applications installed.** - -You will find that Linux, whether running in your own data center or in a public cloud, is the same product form. This is the characteristic of excellent software: achieving a high degree of abstraction. - -Sealos was designed with this in mind. In fact, public and private clouds are essentially the same; they both link computing resources. Many people might think they are different, but public clouds also have recharge and billing. These functions just need to be placed in a separate application, which can be left uninstalled in scenarios where they are not needed. - -![](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-18-vMGZyu.png) - -However, even if a larger enterprise builds a private cloud, it should be similar to a public cloud in form. Metering and billing are very important features. Enterprises with more than 10 people need to finely operate cloud resources, let alone enterprises with thousands of employees using private clouds. The cost allocation among departments is essential. - -Some minor differences, like WeChat payment or third-party login, may indeed be unnecessary, which are just small configurations. - -## An In-Depth Look into Sealos Technology - -Sealos takes on a challenging scenario: allowing multiple tenants to share a Kubernetes cluster in an untrustworthy public network environment. - -The benefits of this approach are significant: -- Users can start directly without building a cluster and only pay for containers, significantly reducing costs. -- As the scale increases, a flywheel effect occurs, drastically reducing marginal costs. (Sealos's Moore's Law: Every doubling of the Sealos cluster size reduces cloud costs for users by 30%) - -However, this approach also presents a significant technical challenge: ensuring isolation, security, and scalability. - -By overcoming these technical challenges, we not only provide great value to customers in the public cloud but also easily handle private cloud scenarios. - -![Sealos Technology Analysis](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-19-sJYA9X.png) - -Decomposing Sealos's technology system: - -### Ubiquitous Operation -A well-known open-source project in this area might be Terraform. Unfortunately, it's slow to start when interfacing with certain clouds (due to poorly written drivers; not Terraform's fault) and can easily hit API call limits of cloud providers (also due to erratic driver requests), failing to meet our needs. - -Moreover, we prefer Kubernetes CRD standards over Terraform. Consequently, we developed our own infrastructure controller. What normally took 10 minutes to launch, we optimized to 30 seconds, which is nearly the limit barring faster server startups by cloud providers. The optimization mainly involved parallel processing and algorithmic adjustments, rather than simple 10-second delays. Additionally, launching a Sealos cluster on these VMs takes only 3 minutes, already outperforming many similar products (which generally take 15 minutes). - -Running on bare metal also requires considering extensive compatibility issues. Thus, Sealos almost entirely abandons RPM, Apt, and other OS-tied installation tools, achieving compatibility with all major Linux distributions. We don't support Windows, simply because we don't prefer it. - -Our cluster imaging capability also effectively supports mainstream hardware architectures like ARM and x86. - -### Cloud Driver Layer -This component faces massive challenges. Without considering isolation and security, installing containerd, Calico, and OpenEBS might suffice. However, in a public, untrusted environment, such weak isolation is unacceptable. Therefore, we've innovated with new technologies, such as Firecracker, for strong container isolation. The challenges of running VMs within VMs in cloud providers' infrastructure will be addressed in a separate article. - -Our network needs require high measurement and isolation standards. Traditional solutions like Calico rely heavily on iptables rules, becoming unusable at scale. We tested and found a 30% failure rate under stress with just 5000 rules. For networking, we introduced Cilium, using eBPF to address these issues and multi-tenant network metering. - -![Cloud Driver Layer](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-19-REW9Fy.png) - -For storage, we use OpenEBS plus LVM, mounting isolated volumes for each user, allowing them to enjoy local disk performance. File storage, however, becomes a significant issue. NFS and similar solutions are nearly toys, unsuitable for production. Thus, we developed Sealfs, a high-performance filesystem, from scratch in Rust, emphasizing simplicity and support for RDMA. - -### Lifecycle Management and Cluster Imaging -For cluster installation and scaling, Sealos users know the process is nearly perfected, with complex clusters managed with a single command! - -Cluster imaging capability, likely unmatched globally, is supported by Sealos and Sealer, both of which I led in development. This capability is king in delivery! Entire clusters can be packaged in a fully Docker-compatible format, enabling one-click deployment in customer environments. - -In the face of our cluster imaging capability, other delivery tools pale in comparison. - -### Tenant Management -Multi-tenancy is a critical need for any enterprise-level user. Designing tenant permissions requires flexibility without complexity, ensuring isolation and collaboration among departments or developers. Native Kubernetes doesn't address these needs, offering only rudimentary namespace management. - -Sealos assigns each user an independent kubeconfig, limiting their permissions to their namespace. Users can share their namespaces but are prohibited from risky actions like piercing through to the host, using privileges, or sharing host filesystem ports. - -### Application Management -Applications are first-class citizens in Sealos, with everything above the cloud kernel being an application. The challenge here is finding a unified application management approach in a multi-tenant environment. - -- Some applications need admin rights to run a controller. -- Others need to run a separate instance. -- Some, like a ChatGPT API, are shared by multiple users. -- Certain applications, like terminal apps, auto-release when unused. - -The system also needs to control and meter these applications, further complicating matters. - -Sealos abstracts these capabilities into applications, similar to running apps on macOS. - -### Proprietary Function Compute Application Laf - -Laf revolutionizes code writing, akin to blogging. - -- Cloud-based development with immediate results. -- Efficient use of Laf means early finishes. -- Even administrative staff can easily use cloud development. -- Function computes come in two types: those that deploy in 30 seconds, and those that discourage use in 30 seconds. - -Laf's millisecond-level publishing outperforms others, where a typical deployment takes 3 to 5 seconds. Laf achieves faster than blogging speeds. - -Laf integrates GPT-4, reducing the need for manual coding. We trained thousands of Laf codes, enhancing AI writing capabilities. - -Databases and object storage are built-in. Except for some AI coding, almost no other concerns are needed. - -It uniquely supports WebSocket, outmatching others (where function compute charges by duration). - -Unlike other function computes that are not persistent, Laf utilizes always-on, auto-scaling containers. - -### Proprietary AI Knowledge Base Application FastGPT - -Laf's AI codes, Sealos's automatic fault diagnosis, AI auto-deployment of applications, and Docker image building rely on FastGPT for knowledge base construction. - -![FastGPT Application](https://jsd.onmicrosoft.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-19-cCinq3.png) - -### Databases/Message Queues Applications -We focus on the operating system and some built-in apps. For highly specialized areas like databases and message queues, we collaborate with top teams. For databases, we partner with Kubeblocks, led by PolarDB founder Cao Wei (conveniently next door, with great coffee and reliable databases). Kubeblocks orchestrates MySQL, PostgreSQL, MongoDB, Redis, etc., ensuring data security and recovery. - -For message queues, we collaborate with RocketMQ founder Wang Xiaorui, addressing RocketMQ and Kafka services. - -In DevOps, we work with Gitea, 90% compatible with GitHub Actions, led by the Gitea creator. The near-perfect compatibility with GitHub Actions is a wise choice. - -### Metering and Billing - -Metering is complex, requiring a monitoring-like collection mechanism. Beyond measuring container CPU, memory, and disk usage, it must associate data with namespaces and ultimately accounts. More challenging measurements include database access counts in function computes and storage used by each tenant. The most difficult is measuring network bandwidth without impacting performance. - -The metering system's challenge is its sensitivity. Any error affects customer billing, necessitating a reconciliation system to verify measurements and ensure billing accuracy. - -With this capability, enterprises can finely operate internal departments and developers, automatically shutting down applications with overdue payments, virtually eliminating idle processes. - -We've also unified the abstraction for metering in both public and private cloud modes. - -## Sealos Design Philosophy and Principles - -The essence of remarkable technology is its simplicity from the user's perspective, similar to the Apple iPhone and the Android operating system. Cloud computing can achieve this too, but simplicity without powerful functionality is merely rudimentary. A brilliant architecture doesn't sacrifice complexity for functionality. - -If Sealos deviates from these principles, it becomes cumbersome and heads towards obsolescence. - -### The Principle of Simplicity - -Sealos embodies simplicity in two aspects: product design and system architecture. - -In terms of product design, we are committed to not burdening users. We want users to use the cloud as easily as they use personal computers, focusing on applications relevant to their roles without being disturbed by unnecessary features. This does not imply weak functionalities; Sealos can enhance system capabilities by extending any application and provides native APIs for flexible expansion. - -At the system architecture level, abandoning the traditional IaaS, PaaS, SaaS three-tier architecture is wise. From an application perspective, there's no need for such complex support structures. With cloud kernel and cloud drivers, **everything above the system is an application.** - -### Fragmentation - -Sealos can be simplified to just a bare Kubernetes, or expand to thousands of applications. It can run on a TV box or in data centers with tens of thousands of servers. Think of Linux, which can operate on embedded devices and in the largest data centers. This represents a fragmented architecture, meeting your exact needs without overwhelming you with unnecessary capabilities. - -Only this architecture can achieve limitless expansion. - -### Modular Assembly - -A cohesive and streamlined architecture allows for customization according to individual needs. Your cloud should be just right – neither too much nor too little. This is possible thanks to a high level of abstraction. Specific functionalities are implemented by applications themselves, while the cloud OS only needs to pool resources and manage applications. Thus, even if there are tens of thousands of applications in the ecosystem, the complexity of your cloud doesn't increase. Think about how you use a smartphone; the cloud can be used in the same way. - -## Use Cases - -### Using Sealos Cloud Services Directly - -- Any business component that can be built into a Docker image can easily run on Sealos (with future AI assistance for image building). This includes projects written in various programming languages within a company. -- One-click launch for highly available clusters of the four major databases: pgsql, mysql, mongo, and redis. Complete with backup, recovery, monitoring, and control. -- Various well-known open-source projects can run on Sealos. - -Thus, Sealos effectively supports your business systems, addressing runtime issues and all backend dependencies. - -### Building a Complete Private Cloud - -It's increasingly apparent that bare metal not only performs better than virtual machines but is also more cost-effective. However, many companies are hesitant to opt for hardware hosting due to the complexity of managing software – be it OpenStack or Kubernetes. - -The cloud service version of Sealos can be cloned exactly into your own data center. Sealos already serves tens of thousands of online users, supporting scenarios and complexities that surpass most companies. After all, companies with tens of thousands of developers are rare. - -So, with bare metal and Sealos, both hardware and software are set, and building a private cloud becomes feasible. - -The cost of self-building? - -- Purchase servers. -- Launch clusters with a single command – so simple anyone can do it, regardless of the number of servers. -- Minimal maintenance, about 0.5 person. We've nearly achieved self-maintenance. The current online cluster serves tens of thousands of applications with approximately 0.1 person's effort in maintenance. - -## Comparison with Other Platforms - -### Comparing with Other Cloud-Native PaaS Platforms - -One of the most frequently asked questions is about the biggest difference in design philosophy. Sealos does not aim to be an all-encompassing PaaS platform. The [cloud operating system](https://sealos.io) itself is highly abstracted, essentially 'nothing'. On Sealos, applications are the prime focus, meeting users' needs through various applications. For instance, when using the Database application on Sealos, you need not worry about any other concepts, not even knowing how to spell 'Kubernetes'. - -Sealos, differing from traditional PaaS platforms that aim for all-inclusiveness, regards the [cloud operating system](https://sealos.io) as a highly abstract 'nothing', emphasizing the importance of applications. On the Sealos platform, **applications are considered the top priority**, catering to a diverse range of user needs. For example, when using the database application on Sealos, users don’t need to concern themselves with any other concepts, **not even how to spell “Kubernetes”**. - -Rancher and KubeSphere are excellent PaaS platforms. However, Sealos does not consider Kubernetes as its core purpose. It focuses more on the applications running on Kubernetes than Kubernetes itself. Hence, Sealos targets a wide range of developers, intending to create a versatile operating system, not confined to serving only the cloud-native sector. Sealos even prefers not to emphasize the yet-to-be-defined concept of 'cloud-native'. - -Therefore, the core idea of Sealos is not “a better Kubernetes” but rather “**providing the applications users need through Kubernetes**”. - -> What do users really need? -> - -In the operating system domain, user needs define the system’s functionalities. The flexibility of an operating system means it does not impose extra burdens on users. For example, Windows is a gaming platform for gamers, a programming tool for programmers, and a graphic processing software for graphic artists. The identity of an operating system is determined by its users, depending on which applications are loaded. Sealos also embraces this philosophy, hence different users will have distinctly different experiences. - -### Comparing Various K8s Installation Tools - -Installation is merely a boot function of the entire operating system, but Sealos also has unique features in cluster lifecycle management and application packaging and delivery. - -Firstly, Sealos can complete installation with a single command. Secondly, using cluster images, the entire cluster can be packaged and delivered anywhere. Lastly, Sealos allows users to flexibly customize their required cluster like writing a Dockerfile, freely assembling and replacing components in the image, offering hundreds of components for user selection. - -## Conclusion - -The Sealos community now has a vast user base, developed over many years, battle-tested, with stability proven in various extreme scenarios, steady as an old dog. - -Our cloud service has seen exaggerated growth in registered users and application numbers, exceeding 6k online developers and nearly ten thousand applications within two weeks of launch. - -We will provide users with a [cloud operating system](https://sealos.io) that is consistent in both public and private cloud experiences, simple, affordable, open, and powerful. \ No newline at end of file diff --git a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/feature.jpg b/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/feature.jpg deleted file mode 100644 index ac9764aaf636..000000000000 Binary files a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/feature.jpg and /dev/null differ diff --git a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop-2.jpg b/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop-2.jpg deleted file mode 100644 index 20166e44d873..000000000000 Binary files a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop-2.jpg and /dev/null differ diff --git a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop.jpg b/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop.jpg deleted file mode 100644 index 14cb1053c6e1..000000000000 Binary files a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop.jpg and /dev/null differ diff --git a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-private-address-on-sealos.jpg b/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-private-address-on-sealos.jpg deleted file mode 100644 index 60025ff8046f..000000000000 Binary files a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-private-address-on-sealos.jpg and /dev/null differ diff --git a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/open-meilisearch-terminal-on-sealos.jpg b/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/open-meilisearch-terminal-on-sealos.jpg deleted file mode 100644 index 7a224efb3e70..000000000000 Binary files a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/open-meilisearch-terminal-on-sealos.jpg and /dev/null differ diff --git a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/star-history-of-meilisearch.png b/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/star-history-of-meilisearch.png deleted file mode 100644 index df050313a35c..000000000000 Binary files a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/star-history-of-meilisearch.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/index.md b/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/index.md deleted file mode 100644 index 9aab30abd4e5..000000000000 --- a/docs/blog/en/2024/how-to-deploy-and-configure-meilisearch-using-docker/index.md +++ /dev/null @@ -1,280 +0,0 @@ ---- -slug: how-to-deploy-and-configure-meilisearch-using-docker -title: "How To Deploy and Configure Meilisearch using Docker" -description: Discover how Meilisearch, the lightning-fast open-source search engine, can transform your app's search experience. Learn installation, features, and integration tips. -authors: [Carson Yang] -tags: [Developer Tools, Sealos, Meilisearch, Flarum] -keywords: [Meilisearch, search engine, fast search, open-source, Flarum, Meilisearch vs Elasticsearch, search engine comparison] -image: ./images/feature.jpg -date: 2024-07-01T10:00 ---- - -## Introduction - -In today's digital world, search functionality is a real game-changer. It's the backbone of virtually every application, making our lives easier and more efficient. From e-commerce platforms to content management systems and corporate knowledge bases, users want to find information quickly and accurately. However, traditional search solutions often fall short, struggling with slow response times, irrelevant results, poor scalability, and lack of flexibility. These issues don't just frustrate users – they can drive them away, ultimately putting a dent in business growth. - -We're so excited to introduce you to [Meilisearch](https://www.meilisearch.com/), an amazing open-source search engine that's here to shake things up! Meilisearch is a fresh newcomer to the search engine scene, and it's already making waves with its easy-to-use deployment, lightning-fast queries, and feature-rich toolkit. Just imagine this: with just one simple command, you can have a Meilisearch server up and running, ready to tackle queries. It's not just powerful; it's also super user-friendly! It's got all these amazing features like fuzzy matching and schema-less indexing, plus a sleek web interface for you to play around with. - - - -In this guide, we're going to take a deep dive into Meilisearch's world. Here's what we'll cover: - -+ the nitty-gritty of Meilisearch's standout features and why they matter. -+ setting up Meilisearch on your system step by step. -+ Meilisearch 101: Essential techniques to get you started. -+ Real-world application: Integrating Meilisearch into a Flarum forum (because theory is great, but practice makes perfect). -+ How Meilisearch stacks up against the competition in the search engine arena. - -## Prerequisites - -Before we get started, we just wanted to make sure you've got these bases covered: - -- You're comfortable navigating the command line -- You've got a grasp on RESTful API concepts -- Bonus points: A [Sealos](https://sealos.io) account for lightning-fast Meilisearch deployment (optional, but handy) -- Ready to revolutionize your search experience? Let's get started with Meilisearch! - -## Introduction to Meilisearch - -[Meilisearch](https://github.com/meilisearch/meilisearch) is a game-changing open-source search engine, it is powered by [Rust](https://www.rust-lang.org/) and offers lightning-fast full-text search capabilities while being incredibly user-friendly and easy to integrate. Meilisearch is built on four key principles that set it apart: - -1. **Blazing Speed**: It delivers results in under 50 milliseconds, no matter how large your dataset is. -2. **Spot-On Relevance**: It uses smart algorithms to ensure the most relevant results always come first. -3. **Developer's Dream**: It features intuitive APIs and clear documentation, making integration a breeze. -4. **Tailor-Made Searches**: It offers a wide range of customization options to fine-tune your search experience. - -Since its launch in 2018, Meilisearch has been on a roll! It's been a real rising star in the open-source community. Thanks to its user-friendly features and exceptional performance, it has already garnered an impressive 40,000+ stars on GitHub. - -![Star history chart of the Meilisearch open source project](./images/star-history-of-meilisearch.png) - -## ## Core Features of Meilisearch - -Meilisearch offers a robust set of search capabilities, making it an ideal choice for projects ranging from personal websites to large-scale enterprise applications. Here's what sets it apart: - -### Lightning-Fast Performance - -- **Blazing Speed**: It delivers results in under 50 ms, no matter how much data you're working with. -- **Instant Feedback**: It even implements search-as-you-type for real-time responsiveness! -- **Typo-Tolerant**: It intelligently handles misspellings, ensuring relevant results despite query errors. - -### Advanced Relevance Features - -- **Custom Ranking**: Tailor result prioritization to your specific needs. -- **Faceted Search**: Enable intuitive filtering and navigation through multi-dimensional data. -- **Synonym Recognition**: Enhance search flexibility by setting up synonyms. - -### Comprehensive Language Support - -- **Global Readiness**: Optimized for a wide array of languages, including complex scripts like Chinese and Japanese. -- **Smart Filtering**: Customizable stop word lists to improve result quality across all supported languages. - -### Powerful Advanced Features - -- **Geo-aware Search**: Incorporate location-based searching and sorting. -- **Secure Multi-tenancy**: Implement robust data segregation with tenant tokens. -- **Result Highlighting**: Emphasize matching text in search outputs. -- **Flexible Document Management**: Easily add, update, or remove documents within your index. - -### Developer-Friendly Ecosystem - -- **Intuitive API**: Offers a RESTful interface for seamless integration. -- **Multilingual SDKs**: Official support across various programming languages. -- **Comprehensive Documentation**: Access detailed guides and practical examples. -- **Self-Hosting Option**: Deploy on your own infrastructure for complete control. - -## Meilisearch vs. Other Search Solutions - -Let's take a closer look at Meilisearch and see how it compares to other leading search solutions! - -| Feature | Meilisearch | Elasticsearch | Algolia | -| ---------------- | -------------- | ------------------------------------------ | --------- | -| Response time | Lightning-fast (<50ms) | Varies, often >100ms | Swift (<100ms) | -| Ease of use | Highly intuitive | Moderate learning curve | User-friendly | -| Typo correction | Built-in | Configurable | Built-in | -| Multilingual support | Excellent | Proficient | Excellent | -| Geospatial search | Supported | Supported | Supported | -| Open-source | Fully open | Partially (some proprietary features) | Proprietary | -| Pricing | Free (self-hosted) | Free (self-hosted), paid cloud options | Paid SaaS | - -While Elasticsearch has a ton of great features and a strong ecosystem, Algolia is all about cloud services and out-of-the-box functionality. But what really sets Meilisearch apart is its speed, simplicity, and open-source commitment. If you're a small to medium-sized project or team looking for the best of both worlds—performance and accessibility—Meilisearch is the perfect fit! - -## Installation and Configuration of Meilisearch - -Setting up Meilisearch is a piece of cake! With a smorgasbord of installation options to fit your needs like a glove, it couldn't be easier. - -One of Meilisearch's standout features is its seamless integration with Docker, making deployment and scalability a breeze. The official Meilisearch Docker image allows you to get a Meilisearch instance up and running with just a single command: - -```bash -$ docker run -p 7700:7700 getmeili/meilisearch:latest -``` - -This Docker-based approach ensures consistent environments across development, testing, and production, significantly simplifying the deployment process. - -Not a tech wizard? No problem! The [Sealos app store](https://sealos.io/docs/guides/templates/) has got your back with one-click deployment templates. It's as easy as pie - just point, click, and you're off to the races! - -**Do you Want to get Meilisearch up and running in a jiffy without getting lost in the weeds of complex setup?** Sealos might be your golden ticket. - -First things first, head over to the [Meilisearch template page](https://template.cloud.sealos.io/deploy?templateName=meilisearch) and hit the "Deploy on sealos" button in the top right corner. - -> New to the [Sealos](https://sealos.io/) scene? No biggie - just sign up for a Sealos cloud account, log in, and you'll be on your way to the deployment page in no time. - -Now, listen up! There are just two settings you need to get right to keep your Meilisearch instance running like a well-oiled machine: - -1. **MEILI_ENV**: This bad boy sets the mood for your instance - either **production** or **development**. - - Production mode: - - Search preview? Nope, it's all business. - - Development mode: - - Search preview's on tap - perfect for tinkering to your heart's content. - -2. **MEILI_MASTER_KEY**: Think of this as your Meilisearch's VIP pass. It locks down everything except `GET /health`. Want in? You'll need to flash those API keys. - - Production mode: - - A master key is non-negotiable. No ifs, ands, or buts. - - Try to skimp on the key (less than 16 bytes), and Meilisearch will throw a tantrum and refuse to play ball. - - Development mode: - - Master key? Take it or leave it. - - No key? It's the Wild West - everything's up for grabs. - -Here's the lowdown: If you don't bring your own key to the party or if it's not up to snuff, Meilisearch will try to hook you up with an auto-generated one. - -Long story short: **In production, a master key is your ride-or-die. In development? It's dealer's choice.** - -Whipping up a key is a walk in the park. Just punch this into your Linux or macOS terminal: - -```bash -openssl rand -base64 48 -``` - -Once you've done that, just hit the "Deploy Application" button and let the magic happen. - -Once everything is all said and done, just head on over to the app details page and hang tight until you see "running." Then, just give that external address a click, and you'll be in Meilisearch search preview heaven in no time! - -All you have to do is enter your master key, and you're all set! - -Here's a little pro tip for you: just give your Sealos desktop (that's the [cloud.sealos.io](https://cloud.sealos.io) page) a quick refresh, and you'll see a shiny new icon! - -![Meilisearch on Sealos Desktop](./images/meilisearch-on-sealos-desktop.jpg) - -Just one click, and you'll be right there with the Meilisearch preview! - -![Meilisearch on Sealos Desktop](./images/meilisearch-on-sealos-desktop-2.jpg) - -Does this feel a little familiar? It's got that Windows shortcut vibe going on, doesn't it? It just goes to show that Sealos can do all the same tricks as your trusty old desktop OS, but with its head in the clouds! - -## Basic Usage of Meilisearch - -Meilisearch offers a user-friendly RESTful API that seamlessly integrates with various programming languages and frameworks. Here's a quick guide to essential operations: - -### 1. Creating an Index - -Start by setting up a search index for your data: - -```bash -$ curl \ - -X POST 'http://localhost:7700/indexes' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_API_KEY' \ - --data-binary '{ - "uid": "movies", - "primaryKey": "id" - }' -``` - -### 2. Adding Documents - -Next, populate your index with some data: - -```bash -$ curl \ - -X POST 'http://localhost:7700/indexes/movies/documents' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_API_KEY' \ - --data-binary '[ - { - "id": 1, - "title": "Carol", - "genres": ["Romance", "Drama"] - }, - { - "id": 2, - "title": "Wonder Woman", - "genres": ["Action", "Adventure"] - } - ]' -``` - -### 3. Performing a Search - -Finally, execute a search query on your indexed documents: - -```bash -$ curl \ - -X POST 'http://localhost:7700/indexes/movies/search' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_API_KEY' \ - --data-binary '{ - "q": "wonder" - }' -``` - -## Integrating Meilisearch into Flarum Forum - -This guide walks you through integrating Meilisearch into your [Flarum](https://template.cloud.sealos.io/deploy?templateName=flarum) forum, giving your users a turbo-charged search experience. - -### 1. Install Meilisearch SDK - -Fire up the terminal from your Flarum app's details page and run. In the Flarum application details interface, click the terminal button in the bottom right corner: - -![Open Meilisearch terminal on Sealos Desktop](./images/open-meilisearch-terminal-on-sealos.jpg) - -Execute the following command in the opened terminal to install the Meilisearch SDK: - -```bash -extension require meilisearch/meilisearch-php -``` - -### 2. Add Scout Search Extension - -In the same terminal, execute: - -```bash -extension require clarkwinkelmann/flarum-ext-scout -``` - -### 3. Set Up Scout Extension - -Head over to your Flarum admin panel. Then, switch on Scout and choose Meilisearch as your search engine. Then, just plug in Meilisearch's URL and key - -If you've got Meilisearch and Flarum in the same Sealos availability zone, you can just use Meilisearch's internal network address, go ahead and enter the Meilisearch application details page. Once you're there, just click on the internal network address to copy Meilisearch's internal network address. Then, paste it as the value of the Scout plugin's Meilisearch Host above. - -![Meilisearch private address on Sealos](./images/meilisearch-private-address-on-sealos.jpg) - -### 4. Beef Up Meilisearch's Memory - -Meilisearch needs a little more space to play nice with Flarum. Here's how to bump it from 128M to 1G: - -1. Find your Meilisearch app details -2. Hit "Update" -3. Crank memory up to 1G -4. Save changes - -### 5. Feed Meilisearch Your Data - -Get your existing content into Meilisearch with: - -```bash -php flarum scout:import-all -``` - -You'll see "Imported" messages for each data type when it's done. - -And there you have it! Your Flarum forum now boasts turbocharged search capabilities, including slick Chinese text search! Let's give it a spin! - -## Conclusion - -Meilisearch is a real powerhouse for developers looking to supercharge their apps with lightning-fast, spot-on search capabilities. Our journey through this tutorial has unveiled Meilisearch's standout features, guided you through the setup process, and shown you how to weave it seamlessly into your projects. - -But hey, don't stop here! We’d love for you to explore even more of the amazing features that Meilisearch has to offer! Have fun tinkering with custom ranking rules and playing around with synonym settings! You'll be amazed at how you can fine-tune your search experience. \ No newline at end of file diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-console.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-console.png deleted file mode 100644 index e82958216382..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-console.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-console2.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-console2.png deleted file mode 100644 index dcb1c5a2143d..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-console2.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-details.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-details.png deleted file mode 100644 index b38e1ae3e83a..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-details.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-logs.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-logs.png deleted file mode 100644 index 82ffc3110928..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-logs.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-on-sealos.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-on-sealos.png deleted file mode 100644 index 8647a6b66052..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-on-sealos.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-on-sealos2.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-on-sealos2.png deleted file mode 100644 index 061cd0eb230f..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-on-sealos2.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-whiteboard.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-whiteboard.png deleted file mode 100644 index dcc99fe0837b..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-whiteboard.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-workspace.png b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-workspace.png deleted file mode 100644 index f40ff9204f47..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE-workspace.png and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE.jpg b/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE.jpg deleted file mode 100644 index 8fea1fd20f14..000000000000 Binary files a/docs/blog/en/2024/how-to-set-up-affine/images/AFFINE.jpg and /dev/null differ diff --git a/docs/blog/en/2024/how-to-set-up-affine/index.md b/docs/blog/en/2024/how-to-set-up-affine/index.md deleted file mode 100644 index a9c5439ec00b..000000000000 --- a/docs/blog/en/2024/how-to-set-up-affine/index.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -slug: how-to-set-up-affine -title: "AFFINE Deployment and Usage Guide: Mastering AFFINE's Self-Hosting Basics" -description: Master AFFiNE self-hosting and experience the future of knowledge management. Our guide takes you through the process, enabling you to harness this integrated KnowledgeOS. -authors: [Carson Yang] -tags: [Notes, Sealos, Wiki] -keywords: [AFFINE, Sealos, Notion, Notes, Wiki, whiteboard] -image: images/feature.jpg -date: 2024-04-07T10:00 ---- - -AFFiNE is a completely open-source alternative to Notion and Miro, with a greater emphasis on privacy and security. Unlike Notion, AFFiNE prioritizes the storage of note content locally. The GitHub repository can be found at [https://github.com/toeverything/AFFiNE](https://github.com/toeverything/AFFiNE). - - - -Built using Rust and TypeScript, AFFiNE offers developers maximum flexibility with a simple, single-command setup. This allows for easy customization and deployment of the application. - -While Miro and Notion focus primarily on whiteboards and pages, AFFiNE positions itself as an integrated KnowledgeOS. It supports kanban boards, tables, and rich text paragraphs as building blocks to create pages or whiteboards, enabling comprehensive document editing, data processing, and brainstorming in one place. - -![](./images/AFFINE.jpg) - -## Key Features of AFFiNE - -### 1. Seamless Integration of Documents and Whiteboards - -AFFiNE stands out as one of the few applications that allows users to place any type of content on a borderless canvas, including rich text, sticky notes, embedded web pages, multi-view databases, linked pages, and even slides. - -Each page in AFFiNE has two views, allowing users to access and edit fully functional blocks anywhere, in any form. - -![](./images/AFFINE-whiteboard.png) - -### 2. Multi-modal AI Copilot - -AFFiNE's AI capabilities can assist with a wide range of tasks, from writing professional work reports and turning outlines into expressive slides to summarizing articles into well-structured mind maps. It can even help users draw and write prototype apps and web pages with just a single prompt. - -### 3. Local-First & Real-Time Collaboration - -AFFiNE adheres to the concept of local-first, emphasizing data privacy. Users have the option to opt out of the cloud service and synchronize their data independently, providing them with greater control over their information. - -### 4. Self-Hosting - -Users can freely fork and build custom versions of AFFiNE or self-host the application. AFFiNE also plans to launch a plugin community and support third-party modules in the future. - -## Self-Hosting AFFiNE - -Self-hosting AFFiNE requires PostgreSQL and Redis databases, which can be complex to set up. However, the [Sealos app store](https://sealos.run/docs/guides/templates/) offers a one-click deployment template that simplifies the process. - -To get started, open this link: - -[![](https://cdn.jsdelivr.net/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](https://bja.sealos.run/?openapp=system-template%3FtemplateName%3Daffine) - -Set the administrator's email (AFFINE_ADMIN_EMAIL) and password (AFFINE_ADMIN_PASSWORD), then click "Go to Sealos deployment" in the upper right corner. - -> New users of [Sealos](https://sealos.io) will need to register and log in to a Sealos public cloud account. After logging in, you'll be redirected to the template deployment page. - -On the deployment page, click "Deploy Application" in the upper right corner to initiate the deployment process. Once the deployment is complete, click "Details" to view the application details page. - -![](./images/AFFINE-on-sealos.png) - -Wait for the instance status to change to "running," then click the log icon to view the logs: - -![](./images/AFFINE-on-sealos2.png) - -A successful startup will display the following log: - -![](./images/AFFINE-logs.png) - -Click on the external network address to open the visual interface of AFFiNE: - -![](./images/AFFINE-details.png) - -By default, AFFiNE uses local browser storage. To log in to your self-hosted instance, click "Log in and enable" in the upper right corner: - -![](./images/AFFINE-console.png) - -After logging in, be sure to click "Enable AFFINE Cloud Service" to avoid data loss, as failing to do so will result in data being stored in the local browser: - -![](./images/AFFINE-console2.png) - -Once the self-hosted cloud service is enabled, you can start using AFFiNE and take advantage of all its features. \ No newline at end of file diff --git a/docs/blog/en/authors.yml b/docs/blog/en/authors.yml deleted file mode 100644 index 1a9418e77f73..000000000000 --- a/docs/blog/en/authors.yml +++ /dev/null @@ -1,18 +0,0 @@ -fanux: - name: fanux - title: '@sealos' - url: https://github.com/fanux - image_url: https://avatars.githubusercontent.com/u/8912557?v=4 - -xiao-jay: - name: xiao-jay - title: '@sealos' - url: https://github.com/xiao-jay - image_url: https://avatars.githubusercontent.com/u/87080562?v=4 - -Carson Yang: - name: Carson Yang - title: '@sealos' - url: https://github.com/yangchuansheng - image_url: https://avatars.githubusercontent.com/u/15308462?v=4 - diff --git a/docs/blog/zh-Hans/2019/08/26/kubernetes-with-sealos.md b/docs/blog/zh-Hans/2019/08/26/kubernetes-with-sealos.md deleted file mode 100644 index e74aee4638d4..000000000000 --- a/docs/blog/zh-Hans/2019/08/26/kubernetes-with-sealos.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -slug: kubernetes with sealos -title: sealos - 以 kubernetes 为内核的云操作系统发行版 -authors: [fanux] -tags: [kubernetes,sealos] ---- - -# 基于云内核的未来云计算架构 - -早期单机操作系统也是分层架构,后面才演化成今天的如 linux windows 的宏内核微内核架构,云操作系统也会有类似发展趋势 - -以前都是单机应用,而现代应用几乎都是分布式应用,kubernetes 已经成为事实上的“云操作系统内核”,能让云内核普及的发型版呼之欲出 - -![image](https://user-images.githubusercontent.com/8912557/176412542-2726e976-617b-4921-a37c-375a814deb8b.png) - -你会发现现在 IaaS PaaS SaaS 在云原生技术普及的浪潮中已经名存实亡,比如容器运行在裸机上就已经拥有非常好的性能了,是否还需要 IaaS 这一层,PaaS SaaS 本质都是容器是否还需要去可以区分,这三层架构已经被击穿! - -程序员很认“鸭式辩型”,就是会游泳长了翅膀的就是鸭子,这种抽象思维是极重要的,这也就是为啥 linux 一切皆文件的设计哲学了。而一个运行的 mysql 集群与一个 crm 软件其实也没有本质区别,所以在云操作系统中,“内核之上皆为应用”。 - -## 云计算三次浪潮 - -基于云内核的云操作系统未来会引发云计算的巨大变革。 - -![image](https://user-images.githubusercontent.com/8912557/176412592-51d55344-0f57-4918-8499-826fa7a7bf28.png) - -先来看看有意思的 web1 web2 web3, 再把互联网的变革套用到云计算中,你会发现,生产关系有非常类似的地方。 - -【1 对 n 关系】 -* web1 : 门户网站生产内容,用户查看内容 -* 云计算 1.0 : 公有云厂商开发服务,企业和开发者使用 - -这个阶段生产关系是 1 对多的,你会发现云厂商几十款云产品是无法满足市场上体量庞大偏好各异的需求的,就像 web1 用户只能看小编写的一些新闻一样。 - ---- -【n 对 1 对 n 关系】 -* web2 : UGC 用户生产内容,用户之间产生链接 -* 云计算 2.0 : 开发者生产云计算应用,给用户使用 - -渐渐的云厂商开始弄 markting place, 一定程度想通过开放市场来连接云计算的生产者与消费者,这就是云计算朝着 2.0 过度的信号, 但是缺乏标准就意味着难以协作,这个阶段想要彻底爆发必须要有“实际上的标准”出现。 - -Docker 镜像算是非常好的标准,但是可惜难以覆盖分布式软件,但是大家通过 docker hub 协作就是一个非常好的协作模型了。 - -kubernetes 的 API 的标准是真正有潜力成为云计算 2.0 事实标准的。未来大家都通过这个系统来相互协作,就像安卓生态蓬勃的应用爆炸一样,这样才能诞生越来越多优质的云服务出来。 - ---- -【n 对 n 关系】 -* web 3 : 网络所有权属于网络的所有参与者,数据回归用户自己手中 -* 云计算 3.0 : 算力属于所有计算的参与者,一台分布式超级计算机诞生 - -整个过程其实是让计算和服务更民主,任何组织个人都可以贡献自己的算力,发布和使用应用的人也不用关心应用到底运行在哪个地方,整个计算的使用像使用一台虚拟计算机一样。 和现在很多大的公链一样,不过目前的智能合约还是场景过于局限,计算成本过高,形式上很像超级计算机,效果上还是差了好几个鸿沟。 - -## 基于云内核设计的云计算会更便宜 - -当前公有云提供的云服务还是极其昂贵的, 在某云厂商官网查到的价格和 IDC 托管硬件相比,如果是存储类型的机器,价格相差十倍!(不过云厂商对大B都有非常大的折扣,小B没有这种福利) - -![image](https://user-images.githubusercontent.com/8912557/176412650-31db6594-a15b-4c95-a47b-9838e9fff6a1.png) - -很多公有云厂商妖魔化私有云,说私有云就不叫云,我想问私有云怎么就不叫云了,是因为私有云太便宜还是私有云动了谁的蛋糕? - -这个价格对比小学生都能算的清楚。其实在云内核设计的云操作系统出现之前公有云确实会便宜,因为软件成本很高,企业想云在自己机房玩一套如 openstack 这样的 IaaS 几乎每年会花费上千万成本,而现在开源生态逐渐成熟让软件成本变得便宜和稳定,私有云的成本便宜逻辑又开始成立了。 - -那还有个问题就是“传统公有云为什么贵?” -* 第一,因为基于的还是 IaaS PaaS SaaS 的架构,每一层都意味着成本,软件的复杂度直接决定成本,所谓的一切自研的优势现在反而会变成成本劣势,这是最主要的原因。 -* 第二,谈边际成本,这个不是按照公有云的用户体量去计算的,而是按照每个可用区的建设成本去计算的,如果软件体系复杂,每个机房需要大量管理节点,需要大量交付人员配合,那成本就无法降下来。但是基于内核设计的云操作系统管理节点只需三台,实习生都能在半个小时以内交付,就像装 centos 一样简单。 -* 第三,次要原因是因为公有云的弹性都是要预留资源的,这部分成本都会摊到消费者头上。 - -很多企业的业务资源使用都是相对固定,半年一年作一次扩容等,托管或者自建肯定会更便宜,促销活动什么的一年也就几次,在促销时使用公有云即可,这样成本可以大幅度降低。 - -## 云计算会走向开源开放 - -封闭的云服务对于企业来说是灾难,最简单的一个场景是应对云厂商的涨价行为,如果强绑定就意味着失去了议价权,近期某云厂商云开发就提价十倍,有些小企业的利润直接就被云服务吃光了。 - -第二个原因是云厂商的云产品如果发展的不好是有可能被下架的,如果企业不幸使用了这类产品,下架时又需要付出巨大迁移成本,有些与代码耦合的甚至需要重写代码。 - -开源自然是开放的最好实现方式,不仅对上面几种场景有比较好的应对措施,关键还可以自由按照自己的需求进行定制。 - -所以未来开源与云是左右腿,像 vercel supabase [sealos](https://github.com/labring/sealos) 这样的产品是云计算的大势所趋。 - -## 基于内核架构的云计算会变得更简单 - -复杂的东西无法普及,复杂的软件要么走向腐烂和消亡,要么重构变得简单,云计算也是如此,你会发现 centos ubuntu 这样的 linux 发行版普及了,但是现在的一些公有云能力很难到处运行和做到普及,即便是开源了,像 openstack 一直未能普及,原因很简单,需要几十个人的团队才能在生产环境玩起来的话绝大多数企业都会放弃。 - -什么叫“内聚”,就是功能不是以牺牲复杂度来换取的,像 linux 的 core 很内聚,驱动即使扩展了一万个系统复杂度也没增加,虽然代码在一直增加。所以软件设计时的抽象能力就变得极重要,基于云内核架构设计的云操作系统也是高“内聚”的,通过扩展应用来扩展能力,而各应用之间是低耦合的。 - -## 内核架构云操作系统爆发时机 - -> 基于开源技术的云服务在侵蚀昂贵且强绑定的公有云的服务 - -现在可以发现公有云云原生领域提供的服务商业化做的好的几乎都是开源强相关的, 如基于 kubernetes 的云服务,基于 prometheus grafana 的可观测服务等。 - -用户越来越聪明了,便宜还是贵按按计算器就能算出来,而且绑定意味着认人鱼肉,技术选型明显往开源技术倾斜。 - -> 云原生侵蚀传统 IaaS 服务 - -基于虚拟机的业务增长速度已经远远赶不上云原生生态的发展速度了,基于 kubernetes 的云原生生态每年几倍甚至有些产品每年几十倍的增长,大量企业在从虚拟机架构往云原生架构迁移。 - -前几年市场被教育的很好,越来越多企业知道云原生降本增效不是一点点,该填的坑也被填的差不多了,开始考虑从观望状态变成实践了。 - -> 市场需要一款云操作系统进一步降低云原生门槛与成本 - -现状是企业在实践云原生的时候还是容易迷失,生态过于庞大复杂,上千款生态软件让很多企业无从下手,而且真要落地至少得有个专家能把云原生计算存储网络都玩的明白,所以这个生态依然还是缺乏好用的开箱即用的发行版。 - -其实这个发行版的要求还是很高的,要非常简单不多不少的去满足客户的需求,还不能给用户带来负担,这就必须得非常好的设计理念和实现机制。 - -## 如何实现这样一个云操作系统 - -如何去设计这样一个操作系统,首先一定需要有非常好的设计理念 - -1. 化整为零,这意味着如果你不装应用,这个系统就是空的,就是 nothing,就是 void*,就和你买了一台新电脑里面除了操作系统什么也没装一样。 -2. 自由组装,所有用户的需求都是通过具体应用实现的而这些应用都是按需求从应用市场中下载,不会硬塞给用户不需要的东西,未能得到满足的需求也是通过应用去扩展。云操作系统不会去追求各种应用风格的统一,就像 macOS 上的微信和飞书不会有统一的风格和账户系统一样,只有这样各应用才能在自己的场景尽情发挥出最大优势。 - -![image](https://user-images.githubusercontent.com/8912557/176412722-87da5eaa-2a27-424f-b429-e2cab7c00eb2.png) - -实现层面,core 是非常内聚的意味它向下仅提供云内核生命周期管理,如安装/伸缩/升级/清理,向上做好应用的打包与管理即可。 - -应用市场方面很重要,一定要有好的标准,这涉及到应用的提供者与消费者之间的协作,OCI registry 仓库就是个非常好的已有事实标准,兼容它是最好的选择。 - -User interface 一定要简单极致,这是用户直接使用你东西的地方,API > CLI > GUI, Desktop 是产品化的终极形态,真的做到用云像用 PC 操作系统一样简单。 - -剩下一切都在于扩展应用的宽度和深度: -1. 广度,常用分布式软件如 mysql 集群,redis 集群,消息队列等逐步覆盖,不断扩展常用分布式应用数量 -2. 深度,基本安装->高可用->可监控->自运维->高性能/安全性->产品化,几个阶段衡量一个分布式应用成熟度 - -那 [sealos](https://github.com/labring/sealos) 就是使用这样的思维去设计的,[laf](https://github.com/labring/laf) 就是 sealos 上的第一款杀手级应用。 - -## 总结 - -未来的云会更便宜 更开放 更简单,最终会有一款优秀的发行版本实现云原生的普及,而 [sealos](https://github.com/labring/sealos) 诞生之日起就朝着这个目标不断进步~ - -相信未来云计算属于所有算力的提供者,云的价值也会属于所有云计算的参与者,不再受任何厂商绑定之苦,更便宜的享受云计算带来的便利。开源开放带给大家简单/便宜的云计算! - -> 作者:fanux.方海涛.中弈 sealos 作者, CNCF sealer 项目发起人。曾就职阿里云,现任环界云计算 CEO, 环界获得陆奇博士奇绩创坛种子轮投资 - diff --git a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/envoy-resource.png b/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/envoy-resource.png deleted file mode 100644 index c3db14f508dc..000000000000 Binary files a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/envoy-resource.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-controller.png b/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-controller.png deleted file mode 100644 index c4835a35efba..000000000000 Binary files a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-controller.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-gateway.png b/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-gateway.png deleted file mode 100644 index e607ff6143bc..000000000000 Binary files a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/higress-gateway.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/nginx-resource.png b/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/nginx-resource.png deleted file mode 100644 index aa36990d2d9c..000000000000 Binary files a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/images/nginx-resource.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/index.md b/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/index.md deleted file mode 100644 index 55d582f63ca8..000000000000 --- a/docs/blog/zh-Hans/2023/how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice/index.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -slug: how-sealos-cloud-mastered-multi-tenancy-with-the-right-gateway-choice -title: Sealos 网关选型血泪史:云原生网关哪家强 -description: 探索 Sealos Cloud 如何在面对数十万条 Ingress 规则和严格的多租户要求中优化其网关选择。从 Nginx Ingress 的局限性到最终选择 Higress,这篇文章深入分析了各种网关选项的性能、稳定性和安全性,为公网环境中支持多租户场景的网关选型提供了宝贵的洞见。 -authors: [fanux] -tags: [Kubernetes, Sealos, 网关] -keywords: [云操作系统, Sealos, K8s, 云原生, 网关, Envoy, Ingress] -image: images/feature.jpg -date: 2024-01-20T10:00 ---- - -[Sealos](https://sealos.run) 公有云几乎打爆了市面上所有主流的开源网关,本文可以给大家很好的避坑,在网关选型方面做一些参考。 - - - -## Sealos Cloud 的复杂场景 - -[Sealos 公有云](https://cloud.sealos.io)上线以来,用户呈爆发式增长,目前总共注册用户 8.7w,每个用户都去创建应用,每个应用都需要有自己的访问入口,就导致整个集群路由条目非常巨大,需要有支撑数十万条 ingress 的能力。 - -另外,在公网提供共享集群的服务,对多租户要求极为苛刻,用户之间的路由必须不能相互影响,需要非常好的隔离性,以及流量控制能力。 - -公有云的受攻击面是很大的,黑客会攻击云上跑的用户应用,也会直接攻击平台的出口网络,安全性上也有非常大的挑战。 - -对控制器的性能和稳定要求都比较高,很多控制器路由条目一多时消耗资源会非常大,甚至 OOM 导致网关奔溃。 - -## 排除 Nginx Ingress - -我们最早用的就是 Nginx Ingress, 最后发现有几个核心问题无法解决: - -* reload 问题,每次有 ingress 变更会导致断连一小会,而一个集群用户一多的时候,ingress 的创建变更会是个频繁事件,就会导致网络经常不稳定。 -* 长链接不稳定,也是因为变更,在用的长链接会经常断。 -* 性能不行,生效时间慢,消耗资源多。 - -所以几乎排除掉了很多底层用 Nginx 实现的网关。我们实测下来基于 Envoy 实现的网关性能彪悍太多,几乎控制面和数据面都不怎么消耗性能。 - -这是 Envoy 的: - -![](images/envoy-resource.png) - -这是 Nginx 的: - -![](images/nginx-resource.png) - -差距非常之大,所以我们就可以排除掉 Nginx 系列选项了。彻底拥抱 Envoy。 - -## 关于 APISIX - -APISIX 本身是个优秀项目,解决了 Nginx reload 的一些问题,所以我们 [Laf](https://laf.run) 早期也用了 APISIX,但是很不幸 APISIX 的 Ingress Controller 并不是很稳定,控制面奔溃给造成了我们好几次大的故障,还出现过控制器 OOM 等问题,我们本来真的很想用,但是最终还是因为故障问题被强制劝退,当然 APISIX 社区也在一直跟进这些问题,希望能越做越好。 - -总结一下就是: APISIX 本身稳定性很好,但是控制器需要优化的东西还很多,稳定性也有待提高。社区支持力度也很大,无奈我们线上问题火烧眉毛没法按照社区的节奏慢慢迭代,只能先切成别的网关了。 - -## Cilium Gateway - -[Sealos 的 CNI](https://sealos.run/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes#%E5%AE%89%E8%A3%85-k8s-%E9%9B%86%E7%BE%A4-1) 很早就切换成 Cilium 了,确实很强,所以我们想着网关也统一用 Cilium 得了,但是现实很骨感。 - -Cilium Gateway 只支持 LB 模式,这样就强依赖云厂商的 LB,而我们也有一些私有化的场景,所以不希望耦合,稳定性方面也遇到了路由非常多的时候 Ingress 生效特别慢的问题,需要分钟级生效,这样用户的体验就很差了,我们能接受的是 5s 内路由生效。所以结论就是只能再等等。 - -## Envoy Gateway - -K8s 标准的发展来看,会逐渐从 Ingress 迁移到 Gateway 的标准,而我们底层又更倾向使用 Envoy,那 Envoy Gateway 的实现似乎是一个很好的选择,所以我们调研了 Envoy Gateway, 但是这个项目还是太过于早期,遇到了一些不稳定的 bug,比如会 OOM,pathpolicy 不生效,有些特性在 merge gateway 模式下不生效等问题,在持续解决中,我们也在不断帮助上游社区提改进意见和贡献,希望未来可以能达到生产可用的状态。 - -## 逼格很高但不那么实用的 Gateway 标准 - -Gateway 的处境很尬感,我的感觉是设计者并没有真的实践过多租户场景,当多租户共享一个集群时,就要明确区分管理者和使用者的权限问题,Gateway 设计之初就没完全考虑清楚,举个例子: - -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: eg -spec: - gatewayClassName: eg - listeners: - - name: http - port: 80 - protocol: HTTP - # hostname: "*.example.com" - - name: https - port: 443 - protocol: HTTPS - # hostname: "*.example.com" - tls: - mode: Terminate - certificateRefs: - - kind: Secret - name: example-com -``` -这里监听端口这类的配置应该是给集群管理员而不是普通用户,而 TLS 证书的配置属于某个应用,管理员可以有权限配置,主要还是每个用户去配置自己的,所以这里面权限就没有分开。 那就只能让用户也有权限配置 Gateway,所以这里就又需要在控制器里实现很多的权限控制的细节问题,如端口号白名单,冲突检测等。 - -个人觉得更优雅的设计是把其中租户级别的字段下沉到 HTTPRoute 中实现,或者一个单独的 CRD,这样用户态和超级管理员就可以分开的更清楚。 现有的方式也能做,就是有点混杂。 - -## 最终 Higress 胜出 - -除了以上重点的项目我们还测试了很多其他项目,我这里就不一一列举了。 Sealos 最终选了 Higress。 - -我们目前选择网关的逻辑很简单,主要就是在满足我们功能的前提下足够稳定,最终选择 Higress 几乎是排除法得出来的。 - -稳定性是排在第一位的,在我们的场景里面能够达到生产可用的目前只有 Higress,不过实践过程中也出现过一些问题,好在 Higress 社区的支持力度很大,很快速的解决了,主要有几个: - -1. ingress 生效速度慢,路由条目多时 2min 多新建路由才能生效,社区最后优化到了 3s 左右,这已经到极致了,也没有再优化的必要了,因为已经比容器 Ready 时间还短了, Higress 使用了一种增量加载配置的机制,让海量路由条目时也能有夸张的性能。 -2. 控制器 OOM,在无动态加载时资源消耗比较大,出现过 OOM 的情况,目前三高问题都解决掉了。 -3. 超时问题,有一个进一步优化加载延时的参数配置 onDemandRDS 在我们一个主集群会偶发请求超时,目前是把该配置关闭了,还在进一步查看原因,而在其它集群中未发现这个问题。 - -安全性方面,我们很多时候的故障问题都是性能问题造成的,流量过大,打爆网关比较常见,所以网关的性能变得至关重要,实测下来 Envoy 要彪悍很多,控制器写的好不好也生死攸关,这个方面 Higress 表现出众: - -![](images/higress-controller.png) - -![](images/higress-gateway.png) - -在我们已经海量路由,超高并发的情况下,需要的资源少的可怜。 - -Higress 还兼容 Nginx Ingress 语法,主要是一些 annotations,我们之前的代码都是用的 Ingress,所以几乎没有任何迁移成本,直接几分钟的升级就可以搞定。 - -同样为了促进社区更好的发展我们也给 Higress 一些意见: - -* 能对 Gateway 的标准有更好的支持,目前虽然已经支持了 v1 版本,但还没有完全兼容 ingress 上的能力。 -* 能开放出一些大杀器的功能,比如安全和熔断方面的能力。让开源和商业结合的更紧密一些,我们倒是不排斥付费,但是随着平台发展,需要更强的一些功能。 -* 周边功能建议更多通过插件机制扩展,让核心功能更内聚一些,简单可依赖。 - -## 总结 - -网关对于云和应用而言是个非常非常核心的组件,随着我们规模的不断扩大也会出现很多新的挑战,我们希望能和上下游社区建立紧密的合作,让开源网关能得到更好的发展,让更多开发者受益。 - -以上列举的很多网关都很优秀,Sealos 没用不代表项目不厉害,只是我们的场景苛刻且奇葩,真的在公网环境能支持多租户的网关并不多,所以各位看官还是要从自己的场景出发,我们的选型仅作参考,同样 Sealos 本身也会以一个开放心态来继续跟进其他网关的发展。 - -最后非常感谢 Higress 开源社区的大力支持,感谢阿里云云原生团队开源了这么优秀的项目,造福广大社区用户。 diff --git a/docs/blog/zh-Hans/2023/k8s-multi-tenancy.md b/docs/blog/zh-Hans/2023/k8s-multi-tenancy.md deleted file mode 100644 index c55f3bb9151c..000000000000 --- a/docs/blog/zh-Hans/2023/k8s-multi-tenancy.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -slug: k8s-multi-tenancy -title: K8s 多租户方案的挑战与价值 -description: 本文深入探讨了 K8s 多租户的概念、其在现代企业中的应用价值,以及实现这一机制所面临的技术挑战和解决方案。 -authors: [fanux] -tags: [Kubernetes, Sealos, 多租户] -keywords: [云操作系统, Sealos, K8s, 云原生, 多租户, 隔离, 命名空间] -image: https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-17-36-fBsk9p.jpg -date: 2023-11-29T10:00 ---- - -在当今企业环境中,随着业务的快速增长和多样化,服务器和云资源的管理会越来越让人头疼。K8s 虽然很强大,但在处理多个部门或团队的业务部署需求时,如果缺乏有效的多租户支持,在效率和资源管理方面都会不尽如人意。 - -**本文将深入探讨 K8s 多租户的概念、其在现代企业中的应用价值,以及实现这一机制所面临的技术挑战和解决方案。** - - - -## K8s 多租户的价值 - -“多租户”是一种软件架构的设计方式,允许多个用户(租户)共享相同的系统或程序组件,同时保持各自数据的隔离性和安全性。在 K8s 环境中,实现有效的多租户机制意味着能够在同一 K8s 集群中运行多个独立的租户工作负载,而无需担心资源冲突、数据泄露或安全问题。 - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-rLPyaY.jpg) - -### 没有多租户支持的挑战 - -当企业购买很多服务器并安装 K8s 后,企业内部多个部门 (例如 20 个) 可能都需要在同一集群上部署各自的业务应用。在没有多租户机制的情况下,这种集群使用方式有很多弊端: - -1. **低效率:**每个部门不能自主使用集群,必须通过集群管理员进行部署和管理。不仅减慢了部署进程,还可能造成排队等待的情况,大大降低工作效率。 - -2. **资源利用不充分:**业务应用不能混合部署,需要对服务器资源进行划分,最终可能会导致资源无法充分利用,造成浪费。 - -3. **业务和资源管理混乱:**在一个没有租户隔离的集群中,各部门的业务相互干扰,难以管理。随着时间的推移,集群的管理和运维变得越来越复杂。 - -4. **规模扩展受限:**在一个单一租户的环境下,集群难以支持多样化的业务需求,限制了企业的扩展能力。 - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-yua4G3.png) - -### 多租户架构的优势 - -一旦有了多租户能力,企业就可以真正意义上构建自己的云环境,实现资源的最大化利用和高效管理: - -1. **资源管理有序:**通过账户系统,每个部门可以自主管理其资源使用,无需担心资源分配和使用上的混乱。 - -2. **效率大幅提升:**各部门可以独立进行业务部署和更新,无需麻烦集群管理员,极大提高了操作效率和业务的灵活性。 - -3. **资源扩展灵活:**集群管理员只需关注整个集群的资源状况,而不是单个业务应用,资源按需分配和扩展会更加灵活和高效。 - -4. **业务隔离保障稳定性:**不同部门的业务应用在集群中彼此隔离,避免了相互干扰,保障了业务的稳定运行。 - -## K8s 多租户的挑战 - -在 K8s 环境中实现多租户架构难度非常大,不是简单使用命名空间的能力就能实现的,还涉及到非常多的技术挑战。 - -### 挑战 1:防止越权 - -在 K8s 多租户环境中,限制每个用户的权限是关键。当多个用户共享一个集群时,一个权限过高的用户可能会对整个集群构成致命威胁。例如,禁止用户访问服务器节点或执行节点级别的操作,如使用 `kubectl get node` 命令。此外,需要限制其他高风险操作,如启用容器特权模式、共享主机文件系统、端口和网络等。 - -为了解决这些问题,Sealos 在其底层架构中采用了多种隔离手段。例如,使用 OpenEBS 进行存储的块级别隔离,Firecracker 以及 Cloud Hypervisor 用于计算运行时的隔离,以及通过 Cilium 实现网络隔离。这些措施确保即使在共享环境中,每个租户的操作也不会影响到其他租户。 - -### 挑战 2:用户的概念、授权与命名空间绑定 - -K8s 本身不具备原生的用户管理系统。因此,需要通过扩展功能来构建用户概念,与第三方用户系统对接,为每个用户生成独立的 kubeconfig 认证文件或 token。此外,需要建立用户与命名空间 (namespace) 之间的多对多关系,并为用户分配适当的权限。 - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-Dfn5xa.png) - -Sealos 的设计允许管理员将用户加入特定的命名空间,并对其角色进行管理,从而有效地控制权限。这样管理员就可以细粒度地管理用户权限,确保每个用户只能访问和修改他们被授权的资源。 - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-wknQxI.png) - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-34-RQFrTB.png) - -### 挑战 3:计量与配额管理 - -在多租户环境中,合理地分配和监控资源使用是另一个重大挑战。需要明确每个租户使用了多少 CPU、内存、磁盘和网络资源,并在资源使用超出配额时进行适当的处理。网络计量尤其复杂,需要区分内外网流量,而且要追踪到达特定容器的流量,并确定这些容器属于哪个租户。 - -Sealos 采用 eBPF 技术来监控网络流量,并通过控制器将流量数据与租户信息相关联,存储到数据库中。这样可以与计量计费系统对接,实现对资源使用的准确计费。对于计算和存储资源的监控,Sealos 同样采用了控制器来收集和管理这些信息。 - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-36-HsycaI.png) - -## Sealos 多租户的挑战 - -如果说上面的这些问题很难解决,那么 **Sealos 的场景是在上述难度上乘以了 10 倍**,因为 Sealos 选择了在公网这个不可信的环境中解决多租户问题,意味着给任意的开发者公开注册,然后一起共享一个 K8s 集群。 - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting6@main/uPic/2023-11-29-10-54-kbCMsN.png) - -公网环境的不可信性和开放性使得实现多租户变得尤为复杂。在这种环境下,任何开发者都可以注册并共享同一个 K8s 集群,这就带来了巨大的安全和稳定性挑战。但是,如果能够成功实现,其好处也是显而易见的: - -1. **成本效益**:用户无需单独搭建和维护完整的集群,显著降低了云服务的使用成本。 -2. **资源优化**:允许每个容器运行在更小的规模上,充分利用平台的弹性和资源。 -3. **强隔离性**:在公网环境中实现良好的多租户隔离,可以确保更高的安全性和稳定性。 - -前途的光明的,但挑战也是巨大的。 - -### 挑战 1:网关限制 - -Sealos 目前拥有数万注册用户,流量很大,我们几乎已经打爆了市面上很多主流的开源网关。在这种情况下,一个用户的更新可能导致所有其他用户受到影响,因为 Nginx 需要重新加载配置,这显然是不能接受的。 - -还有某知名网关 (不便透露),控制器 CPU 很容易就被打爆。 - -还有某知名网关 (同样不便透露),数量一多配置生效需要超过两分钟。不过我们已经和上游社区进行了反馈,应该很快会有改进。 - -### 挑战 2:运行时隔离问题 - -Sealos 需要实现强隔离以保证多租户环境的安全。然而,市面上的主流运行时环境并不能满足 Sealos 的隔离需求。例如,Firecracker 无法提供对 GPU 的良好支持,这对于需要高性能计算的应用来说是一个比较严重的限制。 - -### 挑战 3:存储隔离问题 - -Sealos 需要确保不同租户的数据彼此隔离,防止数据泄露或被其他租户错误访问。这就需要实现块级别的存储隔离,挑战也很大。 - -### 挑战 4:网络计量和争用管理 - -最后,网络资源的计量和管理也是多租户环境中的关键问题。Sealos 需要准确地计量每个用户的网络使用情况,并且在资源有限的情况下合理地分配网络资源。当网络资源产生争用时,需要有机制来公平地解决这些争用,确保所有用户都能公平合理地使用网络资源。 - -## 总结 - -多租户成熟了才能算作是一朵真正的云,才能把云的威力发挥到九成以上。面对公网这一极其复杂和不可预测的环境,Sealos 不仅实现了多租户的隔离和安全,还在保障高效运行的同时,降低了成本。且底层使用了非常多优雅的技术方案,彻底解决企业所有开发者共享一朵云的需求。 diff --git a/docs/blog/zh-Hans/2023/sealos-release.md b/docs/blog/zh-Hans/2023/sealos-release.md deleted file mode 100644 index 948e4514a9a8..000000000000 --- a/docs/blog/zh-Hans/2023/sealos-release.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -slug: sealos-release -title: Sealos - 云操作系统的未来 -description: 深入探索 Sealos 的发展历程,从一个简单的 K8s 安装工具到一个全面的云操作系统项目。了解创始人的故事,Sealos 的演变,以及它如何简化企业和个人的云计算操作。 -authors: [fanux] -tags: [Kubernetes, Sealos] -keywords: [云操作系统, Sealos, K8s, 云原生] -image: https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting5@main/uPic/2023-08-31-09-52-gLmSek.jpg -date: 2023-06-13T10:00 ---- - -## 这是个宏伟的计划 - -这是一个宏伟的计划,漫长且有趣。 - -在今天这个快速发展的云计算领域,Sealos 不仅是一个项目,它是对未来云操作系统概念的重新定义和实践。从一个简单的 K8s 安装工具开始,Sealos 的发展已迈入了全新的领域,目标是构建一个完整、高效且易于管理的[云操作系统](https://sealos.run)。 - -2018 年的某个夜晚,夜深人静,我挥舞键盘,敲下了 Sealos 的第一行代码。当时仓库命名为 “kubeinit”,后来觉得格局太小,我不可能只做一个安装 K8s 的工具。安装只是更大计划的一部分,于是更名为 [Sealos](https://github.com/labring/sealos/ "Sealos"),一个宏大的[云操作系统](https://sealos.run)计划就此诞生! - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting5@main/uPic/2023-08-31-09-52-gLmSek.jpg) - - - -Sealos 的第一个版本写完后,我就把它发布到了阿里云市场出售,**每份售价 15 元**。我没想到真的会有人买,当第一笔 15 元进账时,我异常兴奋,仿佛一个商业帝国就在眼前。但是,结果是我花了一整天时间为这位客户提供售后服务。。。**电影院里还在帮用户解决问题**。 - -先来一波回忆杀: - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-17-59-VHeU8B.jpg) - -随后销量暴增,很快我就换了新手机 iPhone 8,但是问题也同时增加,以至于我根本无法及时提供所有的售后服务。于是我决定重写 Sealos,发布了基于 Ansible 的 v2 版本。最终还是觉得没有做到极致,因为用户还是遇到太多依赖问题无法解决。直到读完 kube-proxy 的源码,我发现有一种方案可以把负载均衡变得更简单,**干掉所有依赖**。于是我编写了 Sealos 的 v3 版本,在安装方面做到了极致。 - -### 为何一开始专注于安装 ? - -因为安装是入口,绝大多数人在学习云原生技术时都无法避开这个问题。**安装的流量入口足够大,无疑是一个绝佳的切入点**。一旦用户习惯使用 Sealos 进行安装,就会逐步探索 Sealos 的其他功能。 - -### 在阿里的工作 - -**在阿里工作期间,我开发了 Sealer**。这里最重要的一点就是,让安装足够灵活。以前用户只能使用我创建的安装包,而集群镜像的创新可以让用户自由定义安装包,也可以自由组合任何安装包。这里有个让我感到自豪的想法 : **把整个集群视为一个整体,把 K8s 看作一个操作系统,那么在这个[云操作系统](https://sealos.run)中,“云版 Docker 镜像”会是什么样子?**这无疑是一个伟大的想法,极具抽象度和灵活性。 - -```dockerfile -FROM kubernetes:v1.25.0 -COPY mysql . -CMD helm install mysql . -``` - -这种构想让**[云操作系统](https://sealos.run)也像单机操作系统一样有了“镜像”**,伟大的构想又完成了一个环节。 - -### 创业第一年 - -那么,Sealos [云操作系统](https://sealos.run)最终会演变成什么样子呢?这是一个难以言状的问题,我只有一个朦胧的设想,隐隐若现。直到创业过程中连续迭代了三个版本,才有了今天的形态——**一切皆应用!** - -理解这一点其实很简单,只需要把单机操作系统上安装的单机应用替换成各种分布式应用即可。整个数据中心,**你看到的不再是一台台孤立的服务器,而是一个整体,变成了一台虚拟的超级计算机。** - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-17-59-ffDeDv.jpg) - -这样简洁、清爽且臻至完美的[云操作系统](https://sealos.run),相信你在第一眼见到它的时候,便会喜欢上它! - -这就是我五年的呕心沥血之作 —— Sealos!献给大家~ - -## 云可以如此干净 - -Sealos 保持了极简的设计,没有任何多余的按钮。实现简洁与强大并行的功能,有时候难如登天,但我们仍在产品设计上投入了大量的心血。无论何人,使用 Sealos 都将沉醉在我们为之打造的舒适体验中。 - -在 B 端软件的世界里,付款者与使用者往往并非同一人,导致产品体验时常被忽略,最关键的还是要说服决策者。而 Sealos 不一样,我们坚信产品体验高于一切,如果我们在产品上花费大量精力最终导致失败,那也死而无憾。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-17-59-v1fPyx.jpg) - -这种黑白灰的设计风格会让你使用产品时**感觉像在喝白开水,而不是在喝饮料,更不是在喝洗脚水** (某些产品使用起来想死的心都有)。开发者已经够痛苦了,我希望你们在使用 Sealos 时心情美好。 - -Sealos 能一针见血地戳中应用的痛点,比如这个应用管理器 App Launchpad,30 秒就可以让你上线自己的应用。这里涉及到大量细节,比如自动配置公网域名,自动解决 HTTPS 证书问题等。 - -## 云可以如此便宜? - -我在 Sealos 上运行了 10 多个应用,包括三个数据库,还有博客,低代码平台,测试平台等,每天只花 4 块钱 : - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-17-59-X4FAjB.jpg) - -为什么能这么便宜? - -- 只需要为运行的容器付费,无需虚拟机,也无需创建整个 K8s 集群,打开直接用。 -- 自动伸缩,夜间用户量少时副本缩小到 1。 -- 我们可以充分利用公有云的弹性,编写大量自动化代码,夜间释放计算资源,降低成本。 - -这对于企业来说,可以减少大量的资源使用成本。我们自己就在 10 台服务器上运行了 7000 多个应用,这意味着什么?企业部署一套 Sealos 集群后,只要服务器资源利用率低于 70% 就可以不断向集群中添加应用,直到填满为止。 - -你可能会问,**为什么不能直接使用 Kubernetes?** 原因很简单,对于诸如讯飞这样的企业,应用分散在各个部门,这时多租户、隔离与协作会变成刚需,直接使用 K8s 会把集群搞乱,最要命的可能是一个部门或者用户不注意搞了个安全问题会让整个集群崩溃,而 Sealos 完美解决了这个问题! - -Sealos 可以帮助 80% 的企业降低 80% 的资源使用成本。 - -## 云可以如此自由 - -与其他管理平台或 PaaS 平台不同,Sealos 的核心设计理念是“一切皆应用”。不同的开发者,不同的角色使用不同的应用,这让每个用户在使用时都没有心智负担。就像安卓生态中有几十上百万个应用,你只关心自己使用的那几款应用,不用关心其他应用在做什么。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-17-59-OsRpZy.jpg) - -这样的设计有两个主要优势: - -### 懂不懂 K8s 都能愉快地使用 Sealos - -许多基于 K8s 的 PaaS 平台或发行版要么暴露大量 K8s 原生概念,要么屏蔽这些概念。这两种做法都不理想。 - -暴露大量原生概念对小白和新手不友好,屏蔽 K8s 则失去了灵活性和兼容性,对 K8s 老司机也非常不友好。 - -Sealos 采取了不同的做法。在这个平台上,不同的人可以使用不同的应用。比如你是开发者想写 CRUD,你可以直接使用 Laf 这个函数应用。如果你是 DBA,你可以直接使用数据库应用。在这种情况下,你完全不需要关心 Kubernetes,这些概念会被完全屏蔽。 - -如果用户是云原生专家,他们可以在 Sealos 上安装 Lens 和各种 K8s Dashboard,也可以打开终端敲各种原生命令。这就极大提高了灵活度。 - -### 自由组装 - -Sealos 非常关注应用间的相互配合。例如,你在 Sealos 上使用函数计算,默认数据库可能是 MongoDB,但如果你想用 PostgreSQL 怎么办?这时就可以在 Sealos 上安装一个 PostgreSQL 应用,然后通过服务发现直接在函数计算里面访问。因为在同一个集群内,可以直接通过内网 DNS 相互配合。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-18-00-HEtGtd.jpg) - -Sealos 精简而不简单,所有组件都可以卸载,这让云恰好满足你的需求——多一分则嫌多,少一分则嫌少。这也意味着无论是一台服务器还是上百台数据中心,都可以通过一条命令构建成一朵云。 - -## Sealos 到底能干什么 - -- 30 秒在 Sealos 上跑个 nginx demo,自动伸缩 -- 30 秒起各种数据库,业务系统内网直接连接数据库 -- 在 Sealos 上直接启动你写的各种编程语言业务 - -这三个能力是基础,其他的能力你可以慢慢探索,慢慢发现新大陆。 - -在运行自己业务上,我们针对这个场景做了很多细节优化,比如自动分配二级域名,自动横向伸缩,支持运行各种有状态服务等。 - -你会发现,借助 Sealos,**无论是部署一个拨测系统,还是运行一个低代码平台,都是信手拈来。您的博客也可以轻松托管在 Sealos 上,成本低廉。使用 Sealos 终端,运行任何兼容 K8s 的应用,自动化操作不再是难题。** - -更进一步发现:原来**有个 AI 在帮你自动做故障诊断,自动上线业务,甚至帮你写代码并自动测试上线**。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-18-01-t4W7LN.jpg) - -然后你会发现普通人也能用 Sealos: - -+ 你可以在 Sealos 上快速安装财务软件; -+ 你也可以在 Sealos 上快速安装知识库,给企业所有人写笔记; -+ 你还可以在 Sealos 上快速安装聊天软件供企业内部沟通协作。 - -到这里你会惊喜地发现:Sealos 竟然什么都能干,真的通用,而且还如此简单!最后你会有所领悟,**原来这就是[云操作系统](https://sealos.run)!** - -## 真的有人在用 Sealos 吗 ? - -当然有,Sealos 社区用户 10 万+,不乏各种大企业。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-16-18-01-ZOZ4Ud.jpg) - -上线两个月时间注册用户已经破万,云服务共计运行 **7000+ 应用**。 - -**只有一些小微应用适合 Sealos 吗 ?** - -当然不,Sealos 的客户中有**国健大数据**,在疫情期间支撑健康码的服务,一秒钟都不能挂的高并发业务。也支撑过超大规模的 GPU 集群,每天处理 80T 数据,整个集群 80PB 数据。**聚道云**上百个应用跑在 Sealos 平台上。 - -## 阶梯计划 - -Sealos 的宏图不止于此,我们的目标是进化为一款无所不在的[云操作系统](https://sealos.run),为人们提供如同使用个人电脑般简易的云服务体验。借助 Sealos,企业可轻松实现: - -- 迅疾如闪电,一分钟上线新业务 -- 一年可缩减半数成本 -- 简单如拨动开关,一键起一朵云 - -企业用云,一款 Sealos 就足矣。 - -未来,我们将继续秉持工匠精神,精心打磨 Sealos 中企业所需的常用应用,如数据库、消息队列、推理能力、各类编程语言执行环境等。 - -Sealos [云操作系统](https://sealos.run)中还会**内置一个 Copilot**,它像一位航海家的副手,它可自动进行云原生改造,帮助开发者轻松迈入云原生的大门,也可以像专家一样帮助诊断集群问题,安全漏洞,并给出专业操作建议。 - -## 总结 - -历经五载,Sealos 总算实现了我当初写下第一行代码时的愿景 —— [云操作系统](https://sealos.run)。 - -感谢第一个为我付了 15块的同学,您的信任与鼓舞犹如一笔巨资,赋予我前行的力量。 - -感谢社区的全体贡献者,尤其是始终与我同行的老崔同学,众行远。 - -感谢讯飞复杂业务场景的锤炼,让我对业务场景有更深入的理解。 - -感谢阿里云在我写 Sealer 时的支持与帮助,为 Sealos 集群镜像的底层能力铸造了坚实的基石。 - -感谢与我共同开启创业之旅的所有伙伴,是大家共同将一颗灵感的种子,种植成为现实的大树。 - -感谢奇绩创坛踹了我临门一脚,也**感谢陆奇博士**出乎意料的看好我们给了我们很大信心。 - -感谢李军院长康一教授,张海龙,高捷资本,金福资产给我们的帮助、指导和信任。 - -感谢每一位选择 Sealos 的用户,你们包容了整个迭代过程中的种种问题,与我们共同雕琢更完美的 Sealos。 - -预祝每一位选择了我们的决策者,此刻你们的决策绝对是明智的。现在,Sealos 已经跨越了全新的起点,未来我们一定不负众望,向大家交付一款完美的[云操作系统](https://sealos.run)。 - -欢迎大家来体验 Sealos [云操作系统](https://sealos.run)的魅力👉 [https://cloud.sealos.io/](https://cloud.sealos.io/) \ No newline at end of file diff --git a/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/feature.jpg b/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/feature.jpg deleted file mode 100644 index 47e90ce07285..000000000000 Binary files a/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/feature.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/sealos-database.png b/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/sealos-database.png deleted file mode 100644 index 96cc662cc039..000000000000 Binary files a/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/images/sealos-database.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/index.md b/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/index.md deleted file mode 100644 index 33401cf0619c..000000000000 --- a/docs/blog/zh-Hans/2023/to-run-or-not-to-run-a-database-on-kubernetes/index.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -slug: to-run-or-not-to-run-a-database-on-kubernetes -title: 在 K8s 上跑数据库,到底有没有意义? -description: 本文深入分析了 K8s 在数据库管理上的优势,包括稳定性、性能和运维效率。了解为什么将数据库集成到 K8s 是当今技术趋势的一部分,并探讨这一选择对企业和开发团队的意义。 -authors: [fanux] -tags: [Kubernetes, Sealos, 数据库] -keywords: [云操作系统, Sealos, K8s, 云原生, 数据库, 容器,] -image: ./images/feature.jpg -date: 2023-12-06T10:00 ---- - -昨天冯老板发了一篇文章探讨了[为什么将数据库放入 K8S 中不是一个明智的选择](https://mp.weixin.qq.com/s/4a8Qy4O80xqsnytC4l9lRg)。 - -如果是四年前有人质疑容器化数据库我觉得还可以 battle 一下,都 2023 年了还有人不能认清这个大势,我就有必要来谈谈我的看法了。 - - - -我从 K8s 0.9 版本时就开始做这件事,当时确实略早,CSI 都不成熟,到 1.0 才稍微稳定点,当时我在科大讯飞工作,负责的项目是建设和维护一整套系统,这套系统最终支撑了公司内部的 PaaS 服务。 - -我们构建了一个 30 台物理机的集群,别看这个集群很小,但是非常有技术含量,里面跑了近 3000 个应用,而且是各种类型的,包括但不限于微服务,数据库,消息队列,缓存等等。**这个集群被公司内部几百名开发人员同时使用,但是整个集群的运维工作只需不到半个人力就能完成,如果没有 K8s 这一切绝对不可能。** - -我们还在不影响上层应用的情况下,无感知地升级了 Linux 内核。这种无感知升级如果没有 K8s 的支持是无法想象的,光是和各个业务线沟通可能都需要半年。 - -我见过另外一个集群,跑了 400 个数据库而已,堆了 400 台服务器和 40 个人的运维团队,集群的整体利用率却不到 10%。整个集群无人敢动,只能一直堆人,人肉运维。这种情况虽然可以归咎于组织的不专业,但实际上,很多团队都面临着类似的挑战,无法有效地管理和优化他们的基础设施。 - -后来我去了阿里,所有的交付类场景数据库全部是跑在 K8s 上。迄今为止我们在容器里跑数据库五年有余,0 故障。 - -## 数据库 on K8s:专业能力的普及化 - -绝大多数做业务的公司对数据库的处理通常存在两个问题:要么是数据库管理水平一般,无法充分发挥数据库的潜能;要么是每年需要在数据库管理上花费大量成本。[数据库 on K8s](https://sealos.run/docs/guides/dbprovider/) 可以让这一切标准化,有了标准,人与人之间才可以协作,生产力改变生产关系,从而大幅提效,让绝大多数不具备专业能力的团队享受到专业能力,本质上分工更明确了,就像农业和畜牧业分离一样,各自专注于自己的领域,从而提高整体的效率和产出。 - -以 KubeBlocks 团队为例,我相信绝大多数公司在数据库层面的积累和专业能力都没有他们强。而且他们将这些实践经验转化为代码,写成了控制器,以极其简单的方式赋能给其他企业。K8s 让这一切成为可能。 - -你可能会问:为什么不用 Ansible?运维人员可能很推崇 Ansible,因为和他们手头上的工具很匹配,用起来很顺手。Ansible 的核心思想是帮助用户部署和执行运维操作,而 K8s 的控制器则是基于另一种思路:**机器能做的事就不应该由人来做**。通过 Operator,可以实现 **24 小时不间断地同步期望状态和实际状态**,而这是用 Ansible 很难实现的,你用 Ansible 实现是想写个定时任务嘛? - -这就像在操作系统诞生之前,程序员需要手动给纸带穿孔来运行程序。有人可能会说,用纸带也能运行程序,甚至可以把程序刻录在光盘上运行,为什么还需要操作系统呢? - -这其实是同样的道理:Ansible 对运维人员来说是一款好工具,但 K8s 的目标是消除低端运维工作 (即编写和执行 Ansible 脚本的工作)。通过 K8s,我们可以实现更高效、更自动化的数据库管理,从而让那些不具备专业数据库管理能力的团队也能享受到专业级的服务。 - -## 数据库 on K8s 的优势 - -大部分人对于在 K8s 上运行数据库的担忧无非就集中在这几个问题上: - -**稳定性不知道怎么样?** - -**出了问题我没法排查?** - -**性能是不是不够好?** - -### 复杂度 - -在 K8s 上运行数据库,复杂度主要分为两个方面: - -1. 建设这套系统的复杂度 -2. 使用上的复杂度 - -**第一:建设这套系统的复杂度** - -如果直接基于原生的 K8s (裸 K8s) 去构建数据库系统,成本会相对较高,而且对于新手来说,这样的操作并不友好,你需要自己建设 K8s 存储驱动、数据库控制器等多个组件,没有深厚的专业知识和实践经验是搞不定的。 - -这个时候发行版的优势就体现出来了,类似于 Linux 系统中,大多数人更倾向于使用 CentOS、Ubuntu 等发行版,而不是直接操作内核。我们也可以将 K8s 视为一种 “云内核”,如果你只是直接使用内核而不进行适当的定制和优化,可能会觉得它不够好用。因为内核本身只是提供了一个框架,很多功能和优化需要用户自己去实现。而 K8s 发行版则帮助用户解决了这一问题。例如,[Sealos 可以帮你一键](https://sealos.run/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes)构建**包括高可用性集群、存储插件和数据库在内的完整系统**。这一切只需要简单的两条命令: - -```bash -$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ - --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] -$ sealos run labring/openebs:v3.9.0 labring/mysql:8.0 -``` - -然后就没有然后了,一个包含高可用集群、存储插件和数据库的系统就诞生了。虽然 Ansible 可以帮助你解决安装问题,但它**无法处理运行时的自愈、多租户等问题**,而 on K8s 可以让数据库 as a Service。 - -**第二:使用上的复杂度** - -通过云操作系统发行版和控制器,用户可以实现产品化的数据库服务,而不是靠脚本解决问题。 - -![](images/sealos-database.png) - -这个页面我相信没有人不会使用吧?即使是菜鸡如我,都有能力建设起一个具有 3 副本的 PostgreSQL 集群,并且包含备份、恢复和监控等功能。这种能力不仅可以赋予企业中的所有开发者,也**展示了 “云计算思维” 与 “脚本思维” 的根本区别**。**云计算让每个人都能够提供服务 (as a Service),而传统的脚本方法只是运维人员的一种便捷工具。** - -### 稳定性 - -我们团队在数据库领域谈不上专业,都能建立起相当稳定的数据库系统,更别说专门研究这个领域的顶尖专家了。这个事情使用者不用操心,扔给专业的人去做就可以了。 - -举个例子,[Sealos 公有云](https://cloud.sealos.io)目前运行了数千个应用,这些应用的数据库都是完全容器化的,由 KubeBlocks 团队提供支持。一旦数据库出现任何问题,我们只需将问题扔给他们即可。从成本角度来看,随便招聘一个 DBA 的成本都远高于我们支付 KubeBlocks 商业版的费用了,而且 Sealos 还是平台的建设方,对于使用数据库的最终用户来说就更不用关心了。从目前的运行情况来看,我们的稳定性已经远超许多非专业团队的运维水平。 - -而且基本上数据库的生命周期管理就那么多事,稳定性问题是会随着时间的推移被收敛的,这些问题不断在代码层面被解决掉,最终用户关心的越来越少。这一点类似于 Linux 系统的稳定性,随着技术的不断成熟和优化,其稳定性已经达到了非常高的水平。**一个良好的软件架构会不断提升和收敛其鲁棒性,并逐渐减少对人的依赖,比如使用 Oracle 的人喝茶时间一定比用开源 MySQL 的人喝茶时间多。** - -所以无论从现实情况还是理论分析来看,稳定性都不应该成为用户在 K8s 上运行数据库的障碍。**将数据库运行在 k8s 上,实际上是在利用几十名顶尖数据库专家的经验,他们将自己的知识和技能沉淀到代码中,以标准化的方式为用户服务。单靠脚本很难将这些经验沉淀得如此彻底和高效。**。单靠脚本很难将这些经验沉淀得如此彻底和高效。 - -### 性能 - -说数据库跑容器性能不好的大概率都是不会玩的,KubeBlocks 团队做过深入的测试与调优,并撰写了很详细的分析文章,很多人觉得真复杂,但是其实这个复杂的事又不需要用户去做。**这些复杂性已经被内嵌在控制器的代码中,对于最终用户来说,这一过程并不复杂**。而且,容器对数据库性能的影响几乎可以忽略不计,真正重要的是磁盘 IO 和网络带宽时延等因素。 - -OpenEBS 裸盘+数据库控制器的方案就可以有效解决性能问题。有了数据库控制器,就无需依赖于分布式存储。控制器能够保证数据库多副本的高性能和高可用性,无论是有状态服务还是无状态服务,对于用户来说都感觉不到差异。如果实例发生故障,控制器会自动进行调整。这才是一种极致的数据库使用体验。 - -[Sealos](https://sealos.run) 目前已经采用了这种解决方案,在保证高可用性的同时,又不牺牲性能。它可以直接对接裸盘,进行自动扩容、备份和恢复。如果节点发生故障,控制器会自动启动新节点,同步数据并将其加入集群。这些高级功能只能在云操作系统中实现,传统的脚本方法只能望尘莫及,而且后者通常还需要人工介入,比如半夜挂了就只能 on call 了。 - -所以**在 K8s 上运行数据库不仅没有性能问题,其稳定性甚至都超过了大多数运维人员的能力**。而且,这种方式已经做到了简单易用和自助操作,你要不要用? - -## 不脱离实际场景去否定和肯定 - -在讨论数据库是否应该容器化时,我们必须考虑不同的实际应用场景。 - -有些公司的数据库已经非常稳定的以非容器化的方式在运行了,也不差钱养着一群数据库专家,这样的情况当然没有动力把数据库搬到 K8s 上,搬出问题谁来背锅?例如,银行通常使用专门的 Oracle 一体机,只需支付订阅费用即可,这样的系统很难有迁移的动力。 - -然而,对于许多业务开发团队和组织来说,他们现在面临着一个新的选择:**以极低的成本获得高度专业的数据库能力,从而将核心团队的精力全部集中在业务开发上。** - -要达到这一效果,他们可以选择直接使用 RDS (关系数据库服务) 这样的数据库云服务,或者采用基于 K8s 的数据库解决方案。这种方法需要一个长时间运行的管理进程来替代人工角色,以赋予那些不懂数据库的团队相应的能力。这就是一个大的趋势,固定成本 (例如开发控制器的成本) 提升了,但是边际成本 (每个使用数据库的团队的成本) 会大幅降低。 - -当前有很多方案可以做到这一点,比如基于虚拟机或基于 Ansible,但毋庸置疑基于 K8s 的控制器在当前看来是最优解。即便是提供类似 RDS 这种能力的服务,底层使用 k8s 技术栈也是最优解。相比之下,虚拟机就不太行了,重,成本自然高,而且有更多的性能消耗。而像 Ansible 这类工具想要实现自助服务和多租户支持,更是异想天开。 - -## 总结 - -### K8s 的重要性 - -K8s 是个大杀器,像是无崖子一甲子的功力你能发挥几成,如果 K8s 不跑数据库,你大概只能发挥 1 成功力。用好 K8s 能够极大地增强数据库运维的效能。 - -### 技术进步带来的分工变革 - -随着技术的不断进步,数据库的管理者和使用者会逐渐分离,传统的人工操作正在逐步被自动化程序所取代。在这个过程中,标准化就成了有效协作的基石。目前没有看到比容器技术和 K8s 更强的事实标准诞生,因此,将数据库跑到 K8s 上是大势所趋。 - -### 实践案例和效益 - -目前已经有很多团队在成本、易用性、稳定性和性能等多个维度上成功实践了 K8s,取得了显著的成果,也尝到了这样做的甜头。由奢入俭难,一旦企业体验到了 K8s 带来的好处,很难再回到传统的运维方式。以 Sealos 为例,从 v2 使用 ansible,到 v3 完全转向 golang,[现在已经发展到 v4 和 v5](https://github.com/labring/sealos),这种技术的演进正是基于 “云计算” 和 “云操作系统” 的思维,而不是传统的 “运维脚本” 思维。脚本连个 API 都实现不了你我谈先进生产力?设计一个系统优先考虑的不一定是给人用的,而是给别的系统调用的,这样整个自动化才能起飞,这就是为什么 API > CLI > GUI 的原因。 - -### 运维角色的转变 - -目前还是有很多存量市场的 DBA 运维人员想保住自己的饭碗在唱衰这个方向,但是英明的决策者迟早会发现采用 K8s 可以大幅降低人力成本,提高效率和系统稳定性。**良禽择木而栖,希望很多运维同学能意识到你们在逐渐被取代是事实,当年我们做讯飞云的时候有近 40 人的运维团队,做完之后连运维这个组都没了**。在阿里云的时候我们团队也是 0 运维人员。 - -### K8s 的快速成熟和生态发展 - -K8s 在以极快的速度走向更成熟,生态在蓬勃发展,诞生了短期的乱象,让落地实践变得无所适从。但是不要担心,优秀的发行版一定会出现,发行版就在做 “熵减” 的事情,简化用户的使用体验,就像 Linux 内核到 Linux 发行版的演进一样,Sealos 就是其中一款基于 K8s 的云操作系统发行版。我最近一段时间回访了将近 200 名 Sealos 的付费用户,没有一个用户反馈上面的数据库不会用的,有反馈不稳定的,几个原因,磁盘满了,升级导致的问题等,这几个问题都被收敛掉了,最终趋近于 0,至少可以说是比用户自己搭建的稳定性高出好几个 9。 - -### 企业的选择 - -企业选不选这样的方案还是根据自己实际情况来判断,但是聪明的企业在尝试数据库 on K8s 之后会带来极大的好处,例如选择了 Sealos + KubeBlocks 的组合,就相当于拥有了: - -1. 一个拥有8年以上经验的专业 K8s 团队。 -2. 一个 P10 带了一帮 P8-9 的顶尖专业数据库团队。 -3. 一个极友好的产品体验,鲁棒性极高,性能极高的数据库系统。 - -连招聘一个专家的成本都不到。当然这种选择一定有阻力,阻力大部分来自于企业内部那些想保住自己饭碗其实可以不太需要的人。 - -我本可以对冯老板的论调逐条反击,但是边看文章边写还是太累了,碎碎念这些,希望看看到底有多少人能有更高级点的认知,希望能听到更多支持 OR 反对我们的声音,一起探索真理~ \ No newline at end of file diff --git "a/docs/blog/zh-Hans/2023/velero\347\232\204\344\275\277\347\224\250.md" "b/docs/blog/zh-Hans/2023/velero\347\232\204\344\275\277\347\224\250.md" deleted file mode 100644 index 9946ea53c64d..000000000000 --- "a/docs/blog/zh-Hans/2023/velero\347\232\204\344\275\277\347\224\250.md" +++ /dev/null @@ -1,149 +0,0 @@ ---- -slug: Use velero to perform cluster backup on k8s -title: Use velero to perform cluster backup on k8s -authors: [xiao-jay] -tags: [kubernetes,sealos] ---- - -## velero的使用 - -文档:https://velero.io/ - -### 注意使用1.24的k8s - -1.25的k8s暂时会有coredns的pod起不来的问题,导致不能备份 - -``` -kubectl get pod -A -kube-system coredns-565d847f94-5b2wt 0/1 CrashLoopBackOff 2271 (4m1s ago) 8d -kube-system coredns-565d847f94-jtw9f 0/1 CrashLoopBackOff 2271 (88s ago) 8d -``` - - - -### 以aws作为存储举例 - -### 1、安装aws cli - -``` -apt install awscli -aws configure #登陆aws -``` - - - -创建bucket - -``` -BUCKET= -REGION= -aws s3api create-bucket \ - --bucket $BUCKET \ - --region $REGION \ - --create-bucket-configuration LocationConstraint=$REGION -``` - - - -### 2、安装velero - -``` -wget https://github.com/vmware-tanzu/velero/releases/download/v1.9.2/velero-v1.9.2-linux-amd64.tar.gz -tar -xvf velero-v1.9.2-linux-amd64.tar.gz -mv velero-v1.9.2-linux-amd64/velero /usr/local/bin/ -``` - - - -### 3、把aws作为备份存储 - -创建一个credentials-velero文件 - -``` -[default] -aws_access_key_id= -aws_secret_access_key= -``` - - - -``` -export BUCKET=xxxx -export REGION=xxxx -velero install \ - --provider aws \ - --plugins velero/velero-plugin-for-aws:v1.5.0 \ - --bucket $BUCKET \ - --backup-location-config region=$REGION \ - --snapshot-location-config region=$REGION \ - --secret-file ./credentials-velero -``` - -查看是否安装成功,availabe就是成功了 - -``` -root@yyj-master1:/home/ubuntu# kubectl get pod -n velero -NAME READY STATUS RESTARTS AGE -velero-7dd66cfb94-pbf47 1/1 Running 0 12m -root@yyj-k8s124-test-master1:/home/ubuntu# velero backup-location get -NAME PROVIDER BUCKET/PREFIX PHASE LAST VALIDATED ACCESS MODE DEFAULT -default aws sealos-test Available 2022-11-10 02:43:28 +0000 UTC ReadWrite true -``` - - - -### 4、Quick Start - -``` -git clone https://github.com/vmware-tanzu/velero.git -cd velero -``` - -#### Basic example (without PersistentVolumes) - -1. Start the sample nginx app: - - ```bash - kubectl apply -f examples/nginx-app/base.yaml - ``` - -2. Create a backup: - - ```bash - velero backup create nginx-backup --include-namespaces nginx-example - ``` - - 然后bucket里面就出现backup - - ![](https://tva1.sinaimg.cn/large/008vxvgGly1h7zu5ykpwxj30pv0ds0ts.jpg) - -3. Simulate a disaster: - - ```bash - kubectl delete namespaces nginx-example - ``` - - Wait for the namespace to be deleted. - -4. Restore your lost resources: - - ```bash - velero restore create --from-backup nginx-backup - ``` - - - -### 5、集群备份 - -每一个小时全部内容备份一次 - -```fallback -velero schedule create full-cluster-backup --schedule="0 * * * *" -``` - - - - - - - diff --git a/docs/blog/zh-Hans/2023/vm-sealos-create.md b/docs/blog/zh-Hans/2023/vm-sealos-create.md deleted file mode 100644 index 14352302d609..000000000000 --- a/docs/blog/zh-Hans/2023/vm-sealos-create.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -slug: using sh quick build a k8s cluster -title: using sh quick build a k8s cluster -authors: [xiao-jay] -tags: [kubernetes,sealos] ---- - -## 使用 multipass 虚拟出虚拟机然后基于 sealos 快速构建一主一从的 k8s 集群, - - -### 前置条件 -1、安装好multipass -如果需要虚拟机把ip暴露出来,需要配置一下网桥 -https://multipass.run/docs/networks-command - -2、~/.ssh必要创建好,使用本地ssh来作为虚拟机的ssh -``` -创建命令`ssh-keygen -t rsa` or `ssh-keygen -t rsa -C "xxxxx@xxx.com"` -``` - - -### 使用教程 - -``` -./start_vm.sh -./start_vm.sh name #指定集群名字 -``` - -脚本内容 - -``` -#!/usr/bin/env sh -# requirement -# ~/.ssh must have id_rsa.pub,id_rsa - -# Usage: -# - sh start_vm.sh -# - sh start_vm.sh Name - -NAME="laf-test" -# if set first param in command line -if [ -n "$1" ]; then - NAME="$1" -fi - -# check if multipass is installed -if ! command -v multipass; then - echo "multipass is not installed, please install it first" - exit 1 -fi - - -MASTER_NAME="$NAME""-master1" -NODE_NAME="$NAME""-node1" -# delete the vm if it already exists -if multipass list | grep -e "^$MASTER_NAME"; then - echo "existing vm $MASTER_NAME" - echo "Deleting the existing vm $MASTER_NAME" - multipass delete -p "$MASTER_NAME" -fi - -if multipass list | grep -e "^$NODE_NAME "; then - echo "existing vm $NODE_NAME" - echo "Deleting the existing vm $NODE_NAME" - multipass delete -p "$NODE_NAME" -fi - -echo "Creating VM..." -#multipass launch --name "$NAME" --cpus 2 --mem 4G --disk 100G -echo "\tmultipass launch --name $MASTER_NAME --cpus 2 --mem 4G --disk 100G " -multipass launch --name $MASTER_NAME --cpus 2 --mem 4G --disk 100G - -echo "\tmultipass launch --name $NODE_NAME --cpus 2 --mem 4G --disk 100G " -multipass launch --name $NODE_NAME --cpus 2 --mem 4G --disk 100G - -if [ $? -eq 0 ]; then - echo "vm is created" -else - echo "ERROR: failed to create vm, please retry" - exit 1 -fi - -# shellcheck disable=SC2139 -alias vm_root_exec="multipass exec $MASTER_NAME -- sudo -u root" -alias vm_node_exec="multipass exec $NODE_NAME -- sudo -u root" -echo "Installing sealos..." -set -x -vm_root_exec -s << EOF -echo "deb [trusted=yes] https://apt.fury.io/labring/ /" | tee /etc/apt/sources.list.d/labring.list -apt update -apt install sealos=4.1.3 -y -EOF -set +x - -set -x -vm_node_exec -s << EOF -echo "deb [trusted=yes] https://apt.fury.io/labring/ /" | tee /etc/apt/sources.list.d/labring.list -apt update -apt install sealos=4.1.3 -y -EOF -set +x - -set -x -cp -r ~/.ssh ~/vm_ssh - -vm_root_exec rm -rf /root/.ssh -vm_root_exec mkdir /root/.ssh -multipass transfer ~/vm_ssh/id_rsa $MASTER_NAME:/home/ubuntu/ -multipass transfer ~/vm_ssh/id_rsa.pub $MASTER_NAME:/home/ubuntu/ -vm_root_exec cp -r /home/ubuntu/id_rsa /root/.ssh/id_rsa -vm_root_exec chmod 600 /root/.ssh/id_rsa -vm_root_exec cp -r /home/ubuntu/id_rsa.pub /root/.ssh/id_rsa.pub - -vm_node_exec rm -rf /root/.ssh -vm_node_exec mkdir /root/.ssh -multipass transfer ~/vm_ssh/id_rsa.pub $NODE_NAME:/home/ubuntu -vm_node_exec cp -r /home/ubuntu/id_rsa.pub /root/.ssh/authorized_keys - -rm -rf ~/vm_ssh - -set +x - -master_ip=$(multipass info "$MASTER_NAME" | grep IPv4: | awk '{print $2}') -node_ip=$(multipass info "$NODE_NAME" | grep IPv4: | awk '{print $2}') - -echo master_ip: $master_ip -echo node_ip: $node_ip - -echo "Installing k8s..." -set -x -vm_root_exec sealos run labring/kubernetes:v1.24.0 labring/helm:v3.8.2 labring/calico:v3.22.1 --masters $master_ip --nodes $node_ip -#vm_root_exec kubectl taint node $NAME node-role.kubernetes.io/master- -#vm_root_exec kubectl taint node $NAME node-role.kubernetes.io/control-plane- -set +x -set +e - -echo "k8s cluster is ready." -``` - diff --git a/docs/blog/zh-Hans/2023/what-is-sealos.md b/docs/blog/zh-Hans/2023/what-is-sealos.md deleted file mode 100644 index 187e92491f77..000000000000 --- a/docs/blog/zh-Hans/2023/what-is-sealos.md +++ /dev/null @@ -1,349 +0,0 @@ ---- -slug: what-is-sealos -title: Sealos vs. 传统云服务:全方位对比分析 -description: 深入探讨 Sealos 的核心功能、技术特点、设计理念,以及它如何革新云操作系统领域。我们也将探索 Sealos 在不同使用场景下的应用,并与市场上其他云服务平台进行比较,以展现其独特的市场优势和潜力。 -authors: [fanux] -tags: [Kubernetes, Sealos] -keywords: [云操作系统, Sealos, K8s, 云原生, 云计算, 分布式, PaaS, Rancher, KubeSphere, 云服务] -image: https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-15-50-TKK1Ol.webp -date: 2023-07-10T10:00 ---- - -随着云计算的快速发展和广泛应用,企业和开发者越来越希望能够灵活、高效地管理和部署云资源。在这个背景下,Sealos 应运而生,它不仅是一个以 K8s 为内核的[云操作系统](https://sealos.run),更是一个创新的解决方案,旨在简化和优化云计算的使用体验。 - -本文将深入探讨 Sealos 的核心功能、技术特点、设计理念,以及它如何革新[云操作系统](https://sealos.run)领域。我们也将探索 Sealos 在不同使用场景下的应用,并与市场上其他云服务平台进行比较,以展现其独特的市场优势和潜力。 - - - -## Sealos 是什么? - -Sealos 在概念上类似于如 Windows 这样的操作系统,但有两个关键的不同点。首先,Sealos 不是在单个服务器上运行,它的核心理念是**将整个数据中心或跨多服务器的资源视为一个统一的整体**。这种方法突破了传统操作系统只在单一机器上运行的局限,将资源和应用管理扩展到了更大规模,能够**在整个数据中心范围内运行和管理应用**,从而大幅提升云资源的利用效率和运维能力。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-13-46-9Nel1a.png) - -与普通操作系统支持的 QQ、微信等日常应用不同,Sealos 专注于为开发者提供所需的分布式应用环境。在 Sealos 的世界里,**复杂的云计算任务变得像使用个人电脑一样简单直观**。无论是运行常见的 Web 服务如 Nginx,还是部署和管理各种编程语言编写的分布式应用,Sealos 都能一键完成,极大地减少了配置和管理的复杂性。它的设计哲学强调用户友好性和简洁性,致力于消除使用云服务时的技术壁垒,让每个用户都能轻松享受到云计算的强大能力。 - -## Sealos 解决的核心问题 - -Sealos 主要解决了以下几个核心问题: - -### 优化用云体验 - -#### 用户界面 - -用户界面 (User Interface) 的重要性不言而喻。传统的单机操作系统为我们提供了一个标准化的用户体验范例。然而,当今众多的云平台已经演变成庞大而复杂的系统,导致用户在产品中迷失方向。甚至催生了一些专门针对云服务的培训岗位,这在某种程度上反映了产品设计的失败。例如,很少有人需要参加苹果手机的使用培训,因为其产品设计已经足够优秀,非常易于理解和操作。 - -在产品设计理念上,我们首先需要认识到不同的用户角色关注点各异。在云服务的用户群体中,有开发者、数据库管理员 (DBA)、运维人员、熟悉 K8s (k8s) 的专家、技术新手和行业专家等。试图用一个产品满足所有这些角色的需求几乎是不可能的。例如,即使是同一个 CI/CD (持续集成/持续部署) 工具,也有人偏爱 Jenkins,而有人更喜欢 Drone。 - -许多 PaaS 平台以 CI/CD 为例,通常集成了特定工具如 Jenkins,这导致一旦该工具不再流行或有更优秀的替代品出现时,平台需重构核心功能。同时,这种设计无法满足不同用户的偏好。 - -单机操作系统的做法值得我们学习。操作系统本身并不过多干预,而是负责良好地管理应用程序。这样,用户就可以自由选择他们喜欢的应用,如办公软件钉钉或飞书,而这与 Windows 操作系统无关。这种方法赋予了用户极大的自由度。尽管许多 PaaS 平台也提供应用市场,但他们并没有将应用视为首要元素。相反,大多数平台将 K8s 视为核心,这不是说有什么大错,但**这种做法只能定位于云原生用户群体,并不能实现高度的抽象**。 - -Sealos 平台则完全遵循了操作系统的理念。它专注于用户所需的具体功能。例如,DBA 在创建数据库时无需关心 K8s 的具体细节;使用函数计算服务的用户不必关心其是否运行在容器中;而对于 K8s 技术专家,则可以通过 Lens 应用程序或命令行工具进行操作。Sealos 的这种设计理念,使得各类用户都能在其平台上找到合适的工具和服务,从而优化了整体的用户体验。 - -#### API > CLI > GUI - -许多人对产品的理解仅限于其 GUI,但实际上,一个没有 API 的云服务产品对企业而言几乎无用。企业为了提高效率,需要打通和对接各种系统,这时 API 的重要性就显现出来了。云服务的设计往往不仅仅是为了人类用户,更多的是为了其他程序或系统,以实现企业操作的高度自动化。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-14-36-qnQmDa.jpg) - -具体来说,Sealos 提供的 API 与 K8s 的 CRD (Custom Resource Definitions,自定义资源定义) 设计完全兼容。用户可以通过 Sealos 的 API,以与操作 K8s 环境相同的方式来管理和控制他们的云资源。为了安全性,Sealos 为每个租户分配了权限受限的 kubeconfig 认证文件。这些文件允许租户在保证安全的前提下,灵活地对接和管理不同的系统和资源。 - -这种设计不仅使得 Sealos 的云服务更加强大和灵活,而且为企业提供了一种高效、自动化的方式来管理他们的云基础设施。通过 API 的广泛应用,企业可以轻松地整合 Sealos 云服务到他们现有的工作流程中,从而提高运营效率和灵活性。 - -#### 快速、高效的操作体验 - -我们的目标是确保大多数操作能在 30 秒内完成,最长不超过 3 分钟。如果某项功能的操作时间超过这个标准,那一定是有问题的,需要重新评估和设计。 - -#### 面向所有用户 - -尽管 Sealos 主要面向开发者,但在功能设计过程中,我们同样关注非技术背景用户的体验。为此,我们特意邀请无技术背景的行政人员亲身体验我们的服务,以此来验证产品的易用性。如果他们能够顺畅完成操作流程,便证明我们的产品操作简便、易于上手。产品的易用性是我们的核心追求,若用户需要他人指导才能使用服务,那就说明我们的设计尚有不足。 - -#### 专注于高质量应用 - -在应用开发领域,Sealos 始终将质量置于数量之上。对绝大多数应用而言,稳定的运行环境及后端数据库支持是不可或缺的。我们致力于先对这些基础应用进行精细打磨,随后才拓展至其他应用领域,融合各个方向、领域的尖端应用,以便为用户提供全面且高效的解决方案。 - -#### 保障扩展性和安全性 - -Sealos 并不自己去设定标准,而是严格遵循成熟的体系和事实标准,这一策略确保了我们的服务与整个云原生生态系统的高度兼容。所有云原生应用均可在 Sealos 上安全运行,即便没有产品化的一些应用也可以通过 Sealos 的终端来运行。我们的兼容性建立在全面支持 K8s 的基础上,同时在安全性方面进行了加强。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-14-48-vbJezM.png) - -在 Sealos 中,为了避免不当操作或不适当的镜像下载对整个系统造成灾难性影响,每位用户的权限被限制在其自身的命名空间内。这种权限管理机制加强了企业级[云操作系统](https://sealos.run)的安全性与稳定性。 - -### 降低用云成本 - -我们的目标不仅是帮您降低 30% 的成本——这样的目标缺乏挑战性,过于枯燥。我们追求的是将云服务的边际成本降至极低,至少要达到原成本的十分之一。如何实现这一目标?这正是我们追求的方向。那么如何实现这个目标呢? - -#### 重定义云架构:摒弃传统模式 - -**首先,我们要摒弃传统的 IaaS、PaaS 和 SaaS 三层架构模式。** - -为何选择放弃这一经典架构?原因在于,传统的分层模式已不再符合当前的技术发展和市场需求。以 IaaS 为例,它通过软件模拟数据中心中的路由器、交换机和虚拟机等硬件,虽然提高了调度的灵活性,但同时也导致软件成本急剧增加。以 OpenStack 为例,没有数十人的团队是难以维护其稳定性的,这直接导致了高昂的软件成本。过去,这种方式似乎是提高资源利用率的必要手段,但现在从应用的角度看,许多应用在运行时并不关心它们是否运行在独立的 VPC 中。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-15-00-2jC2C8.png) - -以上是我五年前画的一张图,现在逐渐变成了现实,这与单机操作系统的发展历程类似:最初是分层的,后来逐渐发展成为更高效的内核架构。云计算的分层架构同样携带着历史的包袱。一旦企业摒弃了 IaaS,它们就可以节省大量成本,并享受到更高的性能。 - -从这个新的视角出发,我们发现,实际上并不需要 IaaS。同时,从技术角度来看,PaaS 和 SaaS 本质上是相同的,它们都是应用层面的服务,因此也无需进行过度区分。在新的云内核架构中,我们只需要有效地实现多租户之间的隔离。这并不需要复杂重量级的解决方案。例如,Sealos 提供了一种在不可信公网环境中实现多租户共享一个 K8s 集群的方式。我们利用强隔离容器 (如 Firecracker)、网络策略 (如 Cilium) 以及存储块设备隔离 (如 OpenEBS) 来实现这一目标,不仅成本更低,效果也更好。 - -![](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-15-02-6N4ygp.png) - -#### 提高应用密度和调度效率 - -除非是非常重型计算密集型的应用,不然一台服务器上不跑个 100 多个应用你出门都不好意思和别人打招呼,而在 Sealos 上我们一台服务器跑 800 个应用!还能保证应用的稳定。 - -这对企业来说非常重要,企业可以显著减少对硬件资源的采购需求。如果你是企业高管,可以重新审视一下公司的整体资源利用率,你会发现大部分情况下这一比率都低于 20%。通过我们的方法,还有很多倍的提升空间。 - -通过 Sealos,企业能够以更简单的方式节省高达一半的成本。 - -#### 充分弹性 - -夜间企业的不活跃应用应该都去睡觉休息,把资源留给离线计算或者训练任务,这点其实用公有云更有优势,因为可以直接释放资源,节省大量成本。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-18-LIZ6Yt.png) - -Sealos 直接把这一重要特性内置。如果企业所有应用都以这样的方式运行,可以节省巨量的成本。 - -#### 干掉僵尸应用和僵尸服务器 - -企业里面有很多开发测试的程序,怎么知道哪些没人在用?甚至还有很多僵尸 “服务器”,有些企业只能靠一个 exel 表来维护谁用了啥,过一段时间把负责都问一遍,清退没人有的服务器,稍微高端点的会搞个老掉牙的 CMDB。。。 - -解决这个问题的釜底抽薪办法是:收钱。对,企业内部也得收钱,欠费的应用就直接干掉。 - -这样每个部门可以申请额度,开发者申请额度,额度用完就干掉应用,这样可以长期保证没有僵尸应用。而 Sealos 把所有服务器都统一纳管,整个集群变成一个大资源池,当然就不可能存在僵尸服务器了。同时还帮企业节省了一帮运维人力。 - -要想杜绝资源浪费就需要这样精细化运营,Sealos 以极低的成本达到这个目的,企业管理者唯一要做的事就是给每个子账户分钱即可。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-18-ijXZ4y.png) - -这样你可以精细化的控制到每个部门每个人用多少钱,从而进一步分析 ROI。 - -#### 最多半个人运维整个云 - -一帮运维?每个业务都有运维组?你见过微软把 PC 卖给你再给你配个运维?所以还是软件不够优秀,足够优秀足够稳定就不需要运维这个角色。或者这个角色做的事情会改变比如写编排文件写 operator。 - -Sealos 的开发者花了不到一半精力维护整个云,8000 个应用时半个人,8w 个也是 80w 个也是 800w 个也是半个人,这就是[云操作系统](https://sealos.run),不会因为体量变大而增加运维复杂度。 - -我是一个比较中性的人但是我有个极端观点是云足够成熟的情况下不应该有运维这个角色,如果你的企业运维超过 3 个人 (搬服务器的除外),那应该好好反思一下。 - -在给现在的运维人员指条明路:去开发[云操作系统](https://sealos.run)。 - -#### 研发人力成本 - -我是一个研发,我至少 50% 以上的精力花在了研发之外的事上,那些杂事加起来可能有 20% 但是其影响可能是 80% 。它会割裂我正在做的事,比如你写完代码想着还要卖服务器,配置证书,打包,上线一想到这些我敢打赌没有哪个开发者喜欢做这些事,除非他是个变态。开发者是群懒人,为了偷懒开发出一大堆工具,这是偷懒者的胜利,sealos 也是一群偷懒者创造的,所以能自动绝不手动,能 AI 绝不人工。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-18-as6HSI.png) - -自己分析问题多累,AI 比人还专业。 - -Sealos 上函数计算能力 Laf 就可以让写代码像写博客一样简单,点击保存,关机走人,要想下班早,Laf 得用好。 - -所有后端依赖如数据库这些都可以轻松 30s 之内解决。未来还有 AI 自动打包上线,自动编码调试等。 - -这无形中的研发效率提升可以节省的成本是难以想象的,我们客户例子中 2 个人干五个人活的比比皆是。 - -### 一键构建私有云,公有云私有云体验一致 - -Sealos 对云计算的理解是深刻的: - -**公有云和私有云是一回事,同一个抽象,同一套代码,同一种体验,装的应用不同尔而已** - -你会发现 linux 不管跑在你自己的数据中心还是跑在公有云上都是一种产品形态,这就是优秀软件的特点,能做到高度抽象。 - -Sealos 设计之初就考虑到这一点,其实公有云与私有云本质是一样的,都是链接计算资源。很多人可能觉得不一样啊,公有云还有充值计费什么的,其实只需要把这些功能放到一个单独的应用中即可,这样在不需要的场景直接不安装这个应用。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-18-vMGZyu.png) - -但其实大一点的企业即便做私有云它的形态也应该和公有云一样,计量计费是非常重要的一个功能,企业超过 10 个人都需要精细化运营云资源,就更别说成千上万人的企业用私有云了,各部门的成本分摊等。 - -一些非常小的差异比如微信支付第三方登录这些可能确实不太需要,这就是一些小的配置。 - -## Sealos 技术分析 - -Sealos 选择一条非常有挑战的场景:在公网不可信的环境中让多租户共享一个 K8s 集群。 - -这样做的好处是巨大的: - -- 用户进来直接用,不需要构建集群,也只需要为容器付费,成本大大降低。 -- 随着规模增大就会产生飞轮效应,让边际成本大量降低。(Sealos 摩尔定律:Sealos 集群规模每翻一倍用户用云成本会降低 30%) - -同时带来了一个巨大的技术挑战:隔离性,安全性和超大规模。 - -我们解决了这个技术挑战,那不仅在公有云上为客户提供很大价值,在私有云场景就更轻松拿捏了。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-19-sJYA9X.png) - -从这张图中拆解 Sealos 的技术体系: - -### 到处运行 - -这块比较著名的开源项目可能是 terraform 了,不过很遗憾在对接某个云时启动速度很慢 (某些 driver 写的不够好,不能怪 terraform),而且很容易触发云厂商的 API 调用 limit (也是 driver 乱请求),无法满足我们的需求。 - -而且我们希望的标准是 K8s CRD 而非 terraform,这样配合上 helm 就可以,于是我们自研了一个对接基础设施的控制器。结果是本需要 10min 启动的基础设施我们优化到了 30s,这几乎是极限了,很难在有压榨空间了,除非云厂商自己服务器启动时间能再快些。优化点主要在一些并行的处理以及退避算法调优,而不是动不动就 sleep 10s。加上在这些虚拟机上启动 Sealos 集群也只需要 3min,这已经优于很多同类产品了,大家可以自行比较 (其它一般 15min) - -同样在裸机上运行要考虑大量兼容性问题,所以 Sealos 底层几乎全部抛弃 rpm apt 这些与操作系统耦合的安装工具,这样几乎兼容了全部主流的 linux 发行版。windows 我们不支持,原因是因为单纯的讨厌不喜欢。 - -同时我们的集群镜像能力可以很好的同时支持 ARM x86 这些主流硬件体系架构。 - -### 云驱动层 - -这块挑战巨大,如果不考虑安全性隔离性,这块装一个 containerd calico openebs 可能就 OK 了,但是在公网不可信环境中这种弱隔离肯定是不行的,所以我们在一些新的技术上填坑,如 firecracker 来解决容器维度强隔离,而云厂商虚拟机里套虚拟机又是有问题的,这里后续我们会单独发一篇文章来介绍。 - -网络我们对计量和隔离的要求极高,而 calico 这些你懂的,隔离会使用大量的 iptables 规则,规模一大基本网络就不可用了,我们测试过 5000 条规则时压力测试一下就有 30% 的失败率。网络这块我们就引入了 cilium,通过 ebpf 来解决这这些问题,还有多租户的网络计量。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-19-REW9Fy.png) - -存储我们使用 openebs + lvm,为每个用户挂载独立隔离的卷,这样用户可以享受到本地磁盘的性能。而文件存储又变成一个大问题,nfs 这些几乎只是玩具,根本无法生产。所以我们世界冠军同学带队基于 rust 完全自研 sealfs 文件系统,架构超级精简,主打高性能,支持 RDMA。 - -### 生命周期管理与集群镜像 - -对于集群安装扩容 Sealos 老用户都知道,几乎已经被做到极致了,多复杂的集群一律一条命令搞定! - -而集群镜像能力世界上可能 (为了不触犯广告法加上可能二字) 只有两款软件支持 Sealos 和 sealer,都是我主导开发的,sealer 在阿里时捐给了 CNCF。这种能力是交付之王!可以用完全兼容 Docker 镜像的格式把整个集群打包,到了客户环境一键交付。 - -在集群镜像能力面前可能 (用词严谨) 其它所有交付工具我只能说:都是弟弟。 - -### 租户管理 - -任何一个企业级用户,多租户都是刚需,在设计租户权限的时候既要灵活又不能复杂,这些部门或者开发者之间既要相互隔离,又需要能相互协作,而原生的 K8s 是不帮你做这些的,只提供一个最粗略的 namespace 管理肯定是不够的。 - -Sealos 会为每个登录的用户分配一个独立的 kubeconfig,其权限只会被限制到用户自己的 namespace 中,用户可以把自己的 namespace 共享给别人。用户想穿透到主机,或者使用特权,或者共享主机文件系统端口号这些危险操作统统禁止,这才是企业实践云原生的 (可能) 最佳姿势。 - -### 应用管理 - -应用是 Sealos 的一等公民,在云内核之上,一切皆应用。这里最难的地方是在多租户的情况下如何寻求一个统一的应用管理方式的抽象。 - -比如有些应用是需要一个管理员权限跑一个控制器的。 - -有些应用是需要跑一个单独的实例的。 - -有些应用是一个实例多个用户共享使用的。如一个 chatGPT API。 - -有些应用是不使用时就要被自动释放的。如 terminal 应用,没人用会自动被回收。 - -而系统还需要对这些应用进行权限的管控和计量,进一步增加了复杂度。 - -Sealos 把这些能力全部抽象成了应用,就像你 macOS 上跑的应用一样。 - -### 自研函数计算应用 Laf - -真的让你体验什么是像写博客一样写代码 - -- 云端开发,码上生花 -- Laf 用的好,天天下班早 -- 行政都会用的云开发 -- 函数计算有两种 30s 上线的和 30s 劝退的 - -Laf 的毫秒级发布几乎吊打一切,一般其他方案发布一次都得等个 3 到 5s,而 laf 能做到比发博客还快。 - -Laf 直接集成 GPT4,大部分代码就不用你自己写了,我们训练了几千份 Laf 的代码,现在 AI 写起来已经很绝绝子了。 - -数据库内置,对象存储内置,除了用 AI 写点代码几乎不用关心其它任何东西。 - -对 websocket 的支持几乎天下无敌 (有些 function 按调用时长收费,我就问你一个长链接你的钱包还够不够) - -别的函数计算不是常驻模型,一个 chatGPT 的会话 ID 你都得存到数据库,而 Laf 完全不需要,用的是常驻自动伸缩容器。 - -### 自研 AI 知识库应用 fastGPT - -Laf AI 写代码,sealos 故障自动诊断,AI 自动上线应用,自动构建 Docker 镜像,这些统统靠 fastGPT 这个项目,自动帮你构建知识库。 - -![](https://jsd.cdn.zzko.cn/gh/yangchuansheng/imghosting-test@main/uPic/2023-11-17-16-19-cCinq3.png) - -### 数据库/消息队列等应用 - -我们核心聚焦在操作系统和部分内置应用上,数据库消息队列这些是个非常专业的领域,我们选择的办法是与顶级天团团队合作,数据库我们选用 kubeblocks,是 polarDB 创始人曹伟主导的项目 (刚好就在我们隔壁,他们的咖啡好喝,数据库好用)。kubeblocks 统一编排了 mysql pgsql mongo redis,各种高可用数据备份恢复等,绝对的为数据保驾护航。数据库管控我们与 bytebase 合作,google **云数据库团队担任技术负责人陈天舟带队。** - -消息队列我们选择与 rocketmq 创始人王小瑞合作,统一解决 rocketmq kafka 等消息队列服务。 - -devops 我们与 gitea 合作,90% 兼容 github actions,gitea 作者亲自带队,github actions 几乎是一个完美 devops 方案,gitea 选择与其兼容是一个非常明智的选择。 - -### 计量计费 - -计量并不容易,首先需要有类似监控系统的采集机制,容器的 CPU 内存磁盘,采集到了之后需要与其 namespace 关联,最终关联到账户。除此之外还需要采集一些比较难采集到的东西,比如函数计算里面数据库的访问次数,对象存储每个租户用的大小等。最难的是在不影响网络性能的情况下对网络带宽的计量。 - -计量系统的挑战是他不像监控系统那样对精准度要求不高,一旦计量出错用户的钱就出错,是个非常敏感操作,我们需要有个对账的系统对计量进行校验,确保没有收错钱。 - -有了这个能力,企业就要以对部门和内部开发者做精细化的运营,应用欠费自动停机就会让整个公司几乎没有僵尸进程。 - -我们还实现了公有云模式与私有云模式计量的统一抽象。 - -## Sealos 设计理念与原则 - -牛掰的东西从使用者视角来看一定是简单的,如苹果手机,安卓操作系统。云计算同样可以做到,但是如果简单功能不强,那叫简陋。牛逼的架构不会牺牲复杂度来增强功能。 - -Sealos 一旦违背这些原则,就会走向笨重,奔向死亡。 - -### 大道至简 - -Sealos 大道至简体现在两个方面:产品设计&系统架构。 - -产品上我们坚决不想给用户带来任何负担,就希望用户像用个人电脑一样用云,是什么样的角色关心什么样的应用,坚决不让额外你不需要用的功能对你产生干扰。这不意味着功能就弱,Sealos 可以通过扩展任何应用来增强系统的能力,也提供原生 API 来自由扩展。 - -系统架构层面抛弃 IaaS PaaS SaaS 三层架构是个明智之举,因为从应用视角来看其实压根就不需要复杂的三层架构去支撑,云内核+云驱动,**系统之上皆为应用。** - -### 化整为零 - -Sealos 可以精简到只剩一个裸 kubernetes,也可以装成千上万个应用,可以跑在电视盒子上,也可以运行在数万台服务器的数据中心,想想优秀的 linux 是不是也是这样,可以运行在嵌入式设备上也可以运行在最大的数据中心,这就是一种化整为零的架构,可以做到恰好满足你的需求,而不是堆了一大堆你不需要的能力。 - -只有这种架构才能做到无限扩展。 - -### 自由组装 - -内聚精简的架构就可以根据自己的需求来做组装,让你的云多一分则嫌多,少一分则嫌少。这得益于高度抽象的能力,具体功能通过应用本身去实现,而[云操作系统](https://sealos.run)本身对下只需要池化资源,对上只需要管理好应用即可,这样即便整个生态几十万应用出现也不会增加你的云的复杂度。参考你是怎么用智能手机的,云也可以这样玩。 - -## 使用场景 - -### 直接使用 Sealos 提供的云服务 - -- 任何的业务组件能 build 成 docker 镜像的就能很轻松跑在 Sealos 上 (后面当然会有 AI 帮助完成镜像构建,什么 Docker 我不懂),比如企业内部各种编程语言写的项目。 -- 四大数据库高可用集群一键启动 pgsql/mysql/mongo/redis,备份/恢复/监控/管控应有仅有。 -- 各种知名开源项目都可运行在 Sealos 上。 - -所以可以很好的在 Sealos 上运行你的业务系统,解决了业务运行时问题和所有后端依赖。 - -### 帮助你构建完整的私有云 - -大家可能越来越发现裸硬件不仅比虚拟机性能更好,而且价格还便宜,不过还是大量公司还不会考虑托管硬件,原因就在于很难搞得定软件部分,搭 openstack?搭 kubernetes?其实都差点意思。 - -Sealos 的云服务版本可以一模一样的 clone 到你自己的机房,Sealos 目前已经服务数万在线用户了,能支持的场景和复杂度已经超过绝大多数公司了,毕竟有几万开发者的公司还是不多的。 - -所以裸机+Sealos 软硬件都有了,自建私有云这个事就成立了。 - -自建成本有多高呢? - -- 买好服务器 -- 一条命令起集群,小白都会,管你多少台服务器都是一键 -- 最多 0.5 人维护,我们基本实现了自运维,目前在线集群服务上万应用投入的维护人力 0.1 个左右 - -## 与其他平台的比较 - -### 对比其他云原生 PaaS 平台 - -这是问的最多的,最大的区别是设计理念,Sealos 不会去做一个大而全的 PaaS 平台,[云操作系统](https://sealos.run)本身是高度抽象的,是 nothing,Sealos 上应用是一等公民,通过各种不同的应用来满足用户的需求,比如 Sealos 上 Database 应用,你在使用它时完全不用关心其他任何概念,kubernetes 单词怎么拼都不知道。 - -与追求大而全的传统 PaaS 平台不同,Sealos 把[云操作系统](https://sealos.run)视为高度抽象化的 “nothing”,强调应用程序的重要性。在 Sealos 平台上,**应用被视为第一等公民**,通过多样化的应用来满足用户的各种需求。例如,使用 Sealos 的数据库应用时,用户无需关注任何其他概念,**甚至不需要知道如何拼写 “Kubernetes”**。 - -Rancher 和 KubeSphere 是非常优秀的 PaaS 平台。但 Sealos 并不将 K8s 作为其核心目的。它更注重于 K8s 上运行的应用,而非 K8s 本身。因此,Sealos 的目标用户是广泛的开发者群体,其意图是打造一个通用性操作系统,不局限于仅服务于云原生领域。Sealos 甚至都不太想强调 “云原生” 这一尚未明确定义的概念。 - -因此,Sealos 的核心思想并非是 “更好用的 Kubernetes”,而是 “**利用 K8s 为用户提供所需的应用**”。 - -> 用户真正需要的是什么? -> - -在操作系统领域,用户的需求定义了系统的功能。操作系统的灵活性意味着它不会给用户带来额外负担。例如,Windows 对于游戏玩家而言是游戏平台,对程序员则是编程工具,对美工来说则是图像处理软件。操作系统的形态由使用者决定,取决于装载了哪些应用。Sealos 也秉承这样的设计理念,因此不同用户的体验将截然不同。 - -### 对比各种 K8s 安装工具 - -安装只是整个操作系统的一个 boot 功能,不过 Sealos 在集群生命周期管理和应用打包交付上也有极大的特色。 - -首先,Sealos 通过一条命令即可完成安装。其次,利用集群镜像,可以将整个集群打包并随处交付。最后,Sealos 允许用户像编写 Dockerfile 一样灵活地定制所需的集群,自由组装和替换镜像中的组件,提供了上百种组件供用户选择。 - -## 总结 - -Sealos 社区现在拥有庞大的社区用户基础,发展了很多年,久经沙场,稳定性在各种极端场景下久经考验,稳如老狗。 - -我们云服务注册用户和应用数量也在夸张级别的增长,上线两周超 6k 在线开发者,近万应用数量。 - -我们会为用户提供一个公有云私有云体验完全一致,简单,便宜,开放,强大的[云操作系统](https://sealos.run)。 \ No newline at end of file diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/access-meilisearch-on-sealos.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/access-meilisearch-on-sealos.jpg deleted file mode 100644 index a522b7e4f4fa..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/access-meilisearch-on-sealos.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/change-meilisearch-mem-on-sealos.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/change-meilisearch-mem-on-sealos.jpg deleted file mode 100644 index 3c01b896c94e..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/change-meilisearch-mem-on-sealos.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/deploy-meilisearch-on-sealos.png b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/deploy-meilisearch-on-sealos.png deleted file mode 100644 index 99e9b0abe16a..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/deploy-meilisearch-on-sealos.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/details-of-meilisearch-on-sealos.png b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/details-of-meilisearch-on-sealos.png deleted file mode 100644 index b309fd40880c..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/details-of-meilisearch-on-sealos.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/feature.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/feature.jpg deleted file mode 100644 index ac9764aaf636..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/feature.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/flarum-admin-panel.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/flarum-admin-panel.jpg deleted file mode 100644 index 9b8833e4a4a2..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/flarum-admin-panel.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/flarum-integrate-meilisearch.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/flarum-integrate-meilisearch.jpg deleted file mode 100644 index 4a701e0a3e90..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/flarum-integrate-meilisearch.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/import-flarum-index-to-meilisearch.png b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/import-flarum-index-to-meilisearch.png deleted file mode 100644 index 833951d01400..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/import-flarum-index-to-meilisearch.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop-2.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop-2.jpg deleted file mode 100644 index 141200ee2016..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop-2.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop.jpg deleted file mode 100644 index 40fbe13567a4..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-on-sealos-desktop.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-private-address-on-sealos.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-private-address-on-sealos.jpg deleted file mode 100644 index 1556c8f99439..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/meilisearch-private-address-on-sealos.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/open-meilisearch-terminal-on-sealos.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/open-meilisearch-terminal-on-sealos.jpg deleted file mode 100644 index 94f537cf841a..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/open-meilisearch-terminal-on-sealos.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/public-address-of-meilisearch-on-sealos.png b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/public-address-of-meilisearch-on-sealos.png deleted file mode 100644 index 1ce6576c94af..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/public-address-of-meilisearch-on-sealos.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/scout-extension-on-flarum.jpg b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/scout-extension-on-flarum.jpg deleted file mode 100644 index 0db407e505b6..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/scout-extension-on-flarum.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/star-history-of-meilisearch.png b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/star-history-of-meilisearch.png deleted file mode 100644 index df050313a35c..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/star-history-of-meilisearch.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/update-meilisearch-on-sealos.png b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/update-meilisearch-on-sealos.png deleted file mode 100644 index ab02ae73f9e8..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/images/update-meilisearch-on-sealos.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/index.md b/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/index.md deleted file mode 100644 index 36f4cfb883cb..000000000000 --- a/docs/blog/zh-Hans/2024/how-to-deploy-and-configure-meilisearch-using-docker/index.md +++ /dev/null @@ -1,294 +0,0 @@ ---- -slug: how-to-deploy-and-configure-meilisearch-using-docker -title: "Elasticsearch 替代品 Meilisearch 安装和使用教程" -description: 本教程探讨了 Meilisearch 的核心特性、安装配置过程,以及如何将其集成到实际应用中。 -authors: [Carson Yang] -tags: [Developer Tools, Sealos, Meilisearch, Flarum] -keywords: [Meilisearch, 搜索引擎, 搜索, 开源, Flarum, Elasticsearch 替代, 搜索引擎对比] -image: ./images/feature.jpg -date: 2024-07-01T10:00 ---- - -如今搜索功能已成为几乎所有应用不可或缺的一部分。无论是电商平台、内容管理系统,还是企业内部知识库,用户都期待能够快速、准确地找到他们需要的信息。然而,传统的搜索解决方案往往面临着诸多挑战:响应速度慢、相关性差、难以适应大规模数据、缺乏灵活性等。这些问题不仅影响用户体验,还可能导致用户流失,最终影响业务增长。 - -为了解决这些问题,我们将为您介绍一款强大而灵活的开源搜索引擎 - [Meilisearch](https://www.meilisearch.com/)。作为一个使用 [Rust](https://www.rust-lang.org/) 语言编写的独立搜索引擎,Meilisearch 以其简单的部署流程、快速的查询响应和丰富的功能集而著称。您只需一个命令行二进制文件,就能运行 Meilisearch 服务器并开始查询,大大简化了搜索引擎的使用门槛。它不仅支持模糊匹配和无模式索引等先进特性,还提供了用于演示目的的 Web 前端界面。 - - - -在本教程中,我们将深入探讨 Meilisearch 的核心优势和实际应用。我们将涵盖以下内容: - -+ Meilisearch 的核心特性和优势概述 -+ 如何安装和配置 Meilisearch -+ Meilisearch 的基本使用方法 -+ 将 Meilisearch 集成到实际应用中(以 Flarum 论坛为例) -+ Meilisearch 与其他搜索解决方案的比较 - -## 先决条件 - -在开始之前,请确保您具备以下条件: - -- 基本的命令行操作知识 -- 对 RESTful API 的基本理解 -- (可选)一个 [Sealos](https://sealos.run) 账户,用于快速部署 Meilisearch - -## Meilisearch 简介 - -[Meilisearch](https://github.com/meilisearch/meilisearch) 是一个使用 **Rust 语言编写**的强大开源搜索引擎。它提供了闪电般快速的全文搜索功能,并且易于使用和集成。Meilisearch 的设计理念围绕着以下核心原则: - -1. **速度至上**:在任何规模的数据集上,都能在 50 毫秒内返回结果。 -2. **相关性优先**:通过智能排序算法,确保最相关的结果总是排在前面。 -3. **开发者友好**:提供直观的 API 和丰富的文档,大大降低了集成和使用的门槛。 -4. **高度可定制**:灵活的配置选项,允许开发者根据具体需求调整搜索行为。 - -自 2018 年首次发布以来,Meilisearch 凭借其简单易用的特性和卓越的性能,迅速在开源社区中脱颖而出。目前,它在 GitHub 上已经获得了超过 40,000 颗星。 - -![开源项目 Meilisearch 的 Star History](./images/star-history-of-meilisearch.png) - -## Meilisearch 的核心特性 - -Meilisearch 提供了丰富多样的搜索功能,可以满足从个人项目到大型企业应用的各种需求。以下是其核心特性: - -### 搜索性能 - -- **闪电般的速度**:无论数据集大小如何,都能在 50 毫秒内返回结果。 -- **即时搜索**:支持 "搜索即输入" (Search-as-you-type) 功能,提供实时反馈。 -- **拼写容错**:智能处理拼写错误,即使查询中存在错误也能返回相关结果。 - -### 相关性优化 - -- **自定义排序**:允许根据业务需求自定义搜索结果的排序规则。 -- **分面搜索**:支持多维度的结果过滤和导航。 -- **同义词管理**:可以设置同义词,提高搜索的灵活性。 - -### 多语言支持 - -- **多语言优化**:针对多种语言进行了优化,包括中文、日文等非拉丁语系。 -- **停用词处理**:可配置停用词列表,忽略对搜索结果影响不大的常见词。 - -### 高级功能 - -- **地理位置搜索**:支持基于地理位置的搜索和排序。 -- **多租户支持**:通过租户令牌实现数据隔离和访问控制。 -- **高亮显示**:在搜索结果中高亮显示匹配的文本。 -- **文档管理**:支持添加、更新和删除索引中的文档。 - -### 开发者友好 - -- **RESTful API**:提供简洁明了的 API,易于集成。 -- **多语言 SDK**:官方提供多种编程语言的 SDK。 -- **详细文档**:提供全面的文档和示例。 -- **可自托管**:支持在自己的基础设施上部署和管理。 - -## Meilisearch vs. 其他搜索解决方案 - -为了更直观地展示 Meilisearch 的优势,我们可以将其与市面上的其他主流搜索解决方案进行对比: - -| 特性 | Meilisearch | Elasticsearch | Algolia | -| ------------ | -------------- | ---------------------------- | --------- | -| 响应速度 | <50ms | 因环境和配置而异,通常>100ms | <100ms | -| 易用性 | 高 | 中 | 高 | -| 自动错误纠正 | 是 | 需配置 | 是 | -| 多语言支持 | 优秀 | 良好 | 优秀 | -| 地理位置搜索 | 支持 | 支持 | 支持 | -| 开源 | 是 | 是(部分功能闭源) | 否 | -| 定价 | 免费(自托管) | 免费(自托管),付费云服务 | 付费 SaaS | - -虽然 Elasticsearch 在功能全面性和生态系统方面略胜一筹,Algolia 则在云服务和开箱即用体验上有优势,但 Meilisearch 在速度、易用性和开源友好度上独树一帜。特别是对于中小型项目和希望完全控制搜索基础设施的团队来说,Meilisearch 提供了一个完美的平衡点。 - -## Meilisearch 的安装和配置 - -Meilisearch 的安装和使用相对简单,提供了多种安装方式以适应不同的环境和需求。 - -对于没有技术背景的同学而言,你也不用担心安装问题,[Sealos 应用商店](https://sealos.run/docs/guides/templates/)提供了一键部署的应用模板,点一下鼠标即可完成部署,非常丝滑。 - -**如果你想快速部署一个 Meilisearch,又不想陷入繁琐的安装和配置过程**,可以试试 Sealos。 - -直接打开 [Meilisearch 应用模板](https://template.bja.sealos.run/deploy?templateName=meilisearch),然后点击右上角的 “去 Sealos 部署”。 - -> 如果您是第一次使用 [Sealos](https://sealos.run/),则需要注册登录 Sealos 公有云账号,登录之后会立即跳转到模板的部署页面。 - -这里有两个关键的环境变量需要特别注意一下,正确配置这些变量可以确保您的 Meilisearch 的安全和性能。 - -1. **MEILI_ENV**:用于配置实例的环境,只能是 **production** 或 **development**。 - - production 模式: - - - 禁用了搜索预览界面。 - - development 模式: - - - 启用了搜索预览功能。 - -2. **MEILI_MASTER_KEY**:用于设置 Meilisearch 的主密钥,它会自动保护除 `GET /health` 之外的所有路由。只有通过 API 密钥才能访问其他端点,包括搜索预览界面。 - - 在 production 模式下: - - - 提供主密钥是强制性的。 - - 如果没有提供主密钥或密钥长度小于 16 字节,Meilisearch 将抛出错误并拒绝启动。 - - 在 development 模式下: - - - 提供主密钥是可选的。 - - 如果没有提供主密钥,所有路由将不受保护且可公开访问。 - -无论在哪种模式下,如果你没有提供主密钥或提供的密钥长度小于 16 字节,Meilisearch 都会建议你使用一个自动生成的主密钥。 - -总结一下:**如果是 production 模式,则必须要提供主密钥;如果是 development 模式,则可以不提供主密钥。** - -密钥的生成非常简单,直接在 Linux 或者 macOS 终端里执行以下命令即可生成一个随机密钥: - -```bash -openssl rand -base64 48 -``` - -填好参数之后,点击右上角的 “部署应用” 开始部署。 - -![Deploy Meilisearch on Sealos](./images/deploy-meilisearch-on-sealos.png) - -部署完成后,直接点击应用的 “详情” 进入该应用的详情页面。 - -![Details of Meilisearch on Sealos](./images/details-of-meilisearch-on-sealos.png) - -等待应用状态变成 running 之后,直接点击外网地址便可打开 Meilisearch 的搜索预览界面。 - -![Public address of Meilisearch on Sealos](./images/public-address-of-meilisearch-on-sealos.png) - -打开之后是这个样子的,输入你设置的密钥,就可以访问了。 - -![Access Meilisearch on Sealos](./images/access-meilisearch-on-sealos.jpg) - -除此之外,还有另外一种打开方式,先刷新 Sealos 桌面 (也就是在 [cloud.sealos.run](https://cloud.sealos.run) 界面刷新浏览器),然后你就会发现 Sealos 桌面多了个图标: - -![Meilisearch on Sealos Desktop](./images/meilisearch-on-sealos-desktop.jpg) - -直接点击这个图标就可以打开 Meilisearch 的搜索预览界面了。 - -![Meilisearch on Sealos Desktop](./images/meilisearch-on-sealos-desktop-2.jpg) - -是不是有点似曾相识?没错,很像 **Windows 的快捷方式!** - -单机操作系统可以这么玩,Sealos 云操作系统当然也可以这么玩。 - -## Meilisearch 的基本使用 - -Meilisearch 提供了直观的 RESTful API,使得与各种编程语言和框架的集成变得非常简单。以下是一些基本操作示例: - -### 1. 创建索引 - -```bash -$ curl \ - -X POST 'http://localhost:7700/indexes' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_API_KEY' \ - --data-binary '{ - "uid": "movies", - "primaryKey": "id" - }' -``` - -### 2. 添加文档 - -```bash -$ curl \ - -X POST 'http://localhost:7700/indexes/movies/documents' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_API_KEY' \ - --data-binary '[ - { - "id": 1, - "title": "Carol", - "genres": ["Romance", "Drama"] - }, - { - "id": 2, - "title": "Wonder Woman", - "genres": ["Action", "Adventure"] - } - ]' -``` - -### 3. 搜索文档 - -```bash -$ curl \ - -X POST 'http://localhost:7700/indexes/movies/search' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_API_KEY' \ - --data-binary '{ - "q": "wonder" - }' -``` - -## 将 Meilisearch 集成到 Flarum 论坛 - -为了展示 Meilisearch 在实际应用中的强大功能,我们将以 Flarum 论坛为例,演示如何接入 Meilisearch 来提升搜索能力。 - -> 关于 Flarum 的安装和使用方法,可参考我们之前写的文章:[Flarum 安装和使用教程](https://zhuanlan.zhihu.com/p/703201822) - -### 1. 安装 Meilisearch SDK - -在 Flarum 应用详情界面,点击右下角的终端按钮: - -![在 Sealos 中打开 Meilisearch 应用终端](./images/open-meilisearch-terminal-on-sealos.jpg) - -在打开的终端中执行以下命令安装 Meilisearch SDK: - -```bash -extension require meilisearch/meilisearch-php -``` - -### 2. 安装 Scout Search 扩展 - -和上面一样,在 Flarum 容器终端中执行以下命令安装 Scout Search 扩展: - -```bash -extension require clarkwinkelmann/flarum-ext-scout -``` - -### 3. 启用 Scout 扩展 - -在 Flarum 管理后台中启用 Scout 扩展,并选择 Meilisearch 作为搜索引擎,index 名称可写可不写。 - -![Flarum admin panel](./images/flarum-admin-panel.jpg) - -下面还需要配置 Meilisearch 的 URL 和密钥。 - -![Scout extension on flarum](./images/scout-extension-on-flarum.jpg) - -如果你的 Meilisearch 和 Flarum 部署在同一个 Sealos 可用区,那么可以直接使用 Meilisearch 的内网地址,进入 Meilisearch 的应用详情页面,点击内网地址便可复制 Meilisearch 的内网地址,然后作为上面 Scout 插件 Meilisearch Host 的值粘贴进去即可。 - -![Meilisearch private address on Sealos](./images/meilisearch-private-address-on-sealos.jpg) - -### 4. 增加 Meilisearch 内存 - -Sealos 中部署的 Meilisearch 默认只给了 128M 内存,如果要接入 Flarum,这个内存是不够用的,需要调整到 1G 才够用。操作方法如下: - -进入 Meilisearch 的应用详情页面,点击右上角的 “变更”: - -![Update Meilisearch on Sealos](./images/update-meilisearch-on-sealos.png) - -将内存调整到 1G,然后点击右上角的 “变更” 即可。 - -![Change Meilisearch memory on Sealos](./images/change-meilisearch-mem-on-sealos.jpg) - -### 5. 导入 Meilisearch 索引 - -使用以下命令将现有数据导入 Meilisearch 索引: - -```bash -php flarum scout:import-all -``` - -下面这种结果就是导入成功了: - -![Import flarum index to Meilisearch](./images/import-flarum-index-to-meilisearch.png) - -最后来测试一下中文搜索功能: - -![Flarum integrate Meilisearch](./images/flarum-integrate-meilisearch.jpg) - -## 总结 - -Meilisearch 为开发者提供了一个强大而灵活的搜索解决方案,特别适合那些需要快速、相关且易于实现的搜索功能的应用。通过本教程,我们探讨了 Meilisearch 的核心特性、安装配置过程,以及如何将其集成到实际应用中。 - -我们鼓励您进一步探索 Meilisearch 的高级功能,如自定义排名规则、同义词设置等,以充分发挥其潜力。同时,关注 Meilisearch 的官方文档和社区更新,以获取最新的功能和最佳实践。 \ No newline at end of file diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-console.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-console.png deleted file mode 100644 index e82958216382..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-console.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-console2.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-console2.png deleted file mode 100644 index dcb1c5a2143d..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-console2.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-details.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-details.png deleted file mode 100644 index 7059e69d704c..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-details.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-logs.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-logs.png deleted file mode 100644 index 82ffc3110928..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-logs.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-on-sealos.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-on-sealos.png deleted file mode 100644 index 8164c91c8380..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-on-sealos.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-on-sealos2.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-on-sealos2.png deleted file mode 100644 index 98af4a97cbac..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-on-sealos2.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-whiteboard.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-whiteboard.png deleted file mode 100644 index dcc99fe0837b..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-whiteboard.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-workspace.png b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-workspace.png deleted file mode 100644 index f40ff9204f47..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE-workspace.png and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE.jpg b/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE.jpg deleted file mode 100644 index 8fea1fd20f14..000000000000 Binary files a/docs/blog/zh-Hans/2024/how-to-set-up-affine/images/AFFINE.jpg and /dev/null differ diff --git a/docs/blog/zh-Hans/2024/how-to-set-up-affine/index.md b/docs/blog/zh-Hans/2024/how-to-set-up-affine/index.md deleted file mode 100644 index 66927c6f5975..000000000000 --- a/docs/blog/zh-Hans/2024/how-to-set-up-affine/index.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -slug: how-to-set-up-affine -title: Notion 开源替代品 AFFINE 部署和使用教程 -description: 本文介绍了开源知识库管理工具 AFFINE 的私有化部署和使用。 -authors: [Carson Yang] -tags: [Notes, Sealos, Wiki] -keywords: [AFFINE, Sealos, Notion, Notes, Wiki, whiteboard] -image: images/feature.jpg -date: 2024-04-07T10:00 ---- - -AFFiNE 是一款完全开源的 Notion + Miro 替代品,与 Notion 相比,AFFiNE 更注重隐私安全,优先将笔记内容保存到本地。 - - - -GitHub 地址:[https://github.com/toeverything/AFFiNE](https://github.com/toeverything/AFFiNE) - -AFFiNE 使用 Rust 和 Typescript 构建,只需要一个命令即可运行整个项目,AFFiNE 以最简单的方式给了所有开发者最大的想象力。 - -与专注于白板和页面的 Miro 和 Notion 不同,AFFiNE 将其定位定义为一体化的 KnowledgeOS。它支持看板、表格和富文本段落作为构建块来形成页面或白板,可以在这里进行文档编辑、数据处理或头脑风暴等综合处理。 - -![](./images/AFFINE.jpg) - -## AFFiNE 的特性 - -### 文档与白板融为一体 - -许多编辑应用声称自己是提高生产力的画布,但 AFFiNE 是为数不多的几款可以让你在无边界画布上放置任何构建块的应用之一 -- 富文本、便签、任何嵌入的网页、多视图数据库、链接页面甚至幻灯片。 - -每个页面都有 2 个视图,你可以在任何地方以任何形式访问和编辑功能齐全的块。 - -![](./images/AFFINE-whiteboard.png) - -### 多模态 AI copilot - -无论是撰写专业的工作报告,还是将大纲转化为富有表现力的幻灯片,亦或是将文章总结为结构良好的思维导图,又或者……只需一个提示,即可直接绘制和编写原型应用和网页,AFFiNE AI 都可以做到。 - -### 本地优先 & 实时协作 - -AFFiNE 遵从本地优先的理念,强调数据的私有性,你完全可以不用它的云服务,自己进行同步。用户有了更多的选择权。 - -![](./images/AFFINE-workspace.png) - -### 私有化部署 - -用户可以随意分叉和构建自定义的 AFFiNE,也可以私有化部署。未来还会推出插件社区和第三方模块。 - -## 私有化部署 AFFiNE - -AFFiNE 的私有化部署依赖 PostgreSQL 和 Redis 数据库,部署起来比较复杂。[Sealos 的应用商店](https://sealos.run/docs/guides/templates/)提供了一键部署的应用模板,点一下鼠标即可完成部署,非常丝滑。 - -直接打开这个链接: - -[![](https://cdn.jsdelivr.net/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](https://bja.sealos.run/?openapp=system-template%3FtemplateName%3Daffine) - -接下来你只需要设置一下管理员的邮箱(AFFINE_ADMIN_EMAIL)和密码(AFFINE_ADMIN_PASSWORD),然后点击右上角的「去 Sealos 部署」。 - -> 如果您是第一次使用 [Sealos](https://sealos.run),则需要注册登录 Sealos 公有云账号,登录之后会立即跳转到模板的部署页面。 - -跳转进来之后,点击右上角的「部署应用」开始部署,部署完成后,直接点击应用的「详情」进入该应用的详情页面。 - -![](./images/AFFINE-on-sealos.png) - -等待实例的状态变成 running 后,点击日志图标查看日志: - -![](./images/AFFINE-on-sealos2.png) - -如果出现了下面的日志,就说明启动成功了: - -![](./images/AFFINE-logs.png) - -这时点击外网地址即可打开 AFFINE 的可视化界面: - -![](./images/AFFINE-details.png) - -打开之后,默认使用的是本地浏览器存储。如果想登录我们自己部署的云服务,需要点击右上角的「登录并启用」: - -![](./images/AFFINE-console.png) - -登录之后,还要再点击「启用 AFFINE Cloud 服务」,不然数据还是继续存储在本地浏览器中,容易丢失: - -![](./images/AFFINE-console2.png) - -启用之后,我们就可以愉快地使用 AFFINE 啦。 \ No newline at end of file diff --git a/docs/blog/zh-Hans/authors.yml b/docs/blog/zh-Hans/authors.yml deleted file mode 100644 index 1a9418e77f73..000000000000 --- a/docs/blog/zh-Hans/authors.yml +++ /dev/null @@ -1,18 +0,0 @@ -fanux: - name: fanux - title: '@sealos' - url: https://github.com/fanux - image_url: https://avatars.githubusercontent.com/u/8912557?v=4 - -xiao-jay: - name: xiao-jay - title: '@sealos' - url: https://github.com/xiao-jay - image_url: https://avatars.githubusercontent.com/u/87080562?v=4 - -Carson Yang: - name: Carson Yang - title: '@sealos' - url: https://github.com/yangchuansheng - image_url: https://avatars.githubusercontent.com/u/15308462?v=4 - diff --git a/docs/blog/zh-Hans/options.json b/docs/blog/zh-Hans/options.json deleted file mode 100644 index c57e381cb31e..000000000000 --- a/docs/blog/zh-Hans/options.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": { - "message": "博客", - "description": "The title for the blog used in SEO" - }, - "description": { - "message": "Sealos 云操作系统正式发布!", - "description": "The description for the blog used in SEO" - }, - "sidebar.title": { - "message": "最近的博客", - "description": "The label for the left sidebar" - } -} \ No newline at end of file diff --git a/docs/website/docusaurus.config.js b/docs/website/docusaurus.config.js index 94d64fb35278..3a12ff3ab07c 100644 --- a/docs/website/docusaurus.config.js +++ b/docs/website/docusaurus.config.js @@ -75,32 +75,34 @@ const config = { themeConfig: { // @type {import('@docusaurus/preset-classic').ThemeConfig} metadata: [{ name: 'title', content: 'Sealos by 环界云' }], - announcementBar: { - id: 'sealos_tip', - content: ` -
-
${isDomesticSite ? 'If you are an international user, please visit 👉' : '如果您是国内用户,请直接访问 👉 '}
-
- ${isDomesticSite ? 'International Site' : '国内官网'} + ...(isDomesticSite ? {} : { + announcementBar: { + id: 'sealos_tip', + content: ` +
+
如果您是国内用户,请直接访问 👉
+
+ 国内官网 +
+ + + + + + + + + + + +
- - - - - - - - - - - - -
- `, - isCloseable: true, - }, + `, + isCloseable: true, + } + }), algolia: { // Algolia 提供的应用 ID appId: "SLTSB7B9Y0", diff --git a/docs/website/package-lock.json b/docs/website/package-lock.json new file mode 100644 index 000000000000..cca3c3f30d10 --- /dev/null +++ b/docs/website/package-lock.json @@ -0,0 +1,14317 @@ +{ + "name": "sealos-site", + "version": "5.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sealos-site", + "version": "5.0.0", + "dependencies": { + "@docusaurus/core": "^2.4.3", + "@docusaurus/preset-classic": "^2.4.3", + "@docusaurus/theme-search-algolia": "^2.4.3", + "@headlessui/react": "^1.7.17", + "@mdx-js/react": "^1.6.22", + "autoprefixer": "^10.4.16", + "docusaurus-plugin-sass": "^0.2.2", + "dotenv": "^16.3.1", + "postcss": "^8.4.31", + "prism-react-renderer": "^1.3.5", + "prismjs": "^1.29.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-helmet": "^6.1.0", + "sass": "^1.62.1", + "tailwindcss": "^3.3.3", + "wowjs": "^1.1.3" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^2.4.3", + "@tsconfig/docusaurus": "^1.0.6", + "babel-plugin-prismjs": "^2.1.0", + "typescript": "^5.0.4" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.8.2.tgz", + "integrity": "sha512-mTeshsyFhAqw/ebqNsQpMtbnjr+qVOSKXArEj4K0d7sqc8It1XD0gkASwecm9mF/jlOQ4Z9RNg1HbdA8JPdRwQ==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.8.2" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.8.2.tgz", + "integrity": "sha512-J0oTx4me6ZM9kIKPuL3lyU3aB8DEvpVvR6xWmHVROx5rOYJGQcZsdG4ozxwcOyiiu3qxMkIbzntnV1S1VWD8yA==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.8.2" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.8.2.tgz", + "integrity": "sha512-b6Z/X4MczChMcfhk6kfRmBzPgjoPzuS9KGR4AFsiLulLNRAAqhP+xZTKtMnZGhLuc61I20d5WqlId02AZvcO6g==", + "license": "MIT" + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.17.1.tgz", + "integrity": "sha512-e91Jpu93X3t3mVdQwF3ZDjSFMFIfzSc+I76G4EX8nl9RYXgqcjframoL05VTjcD2YCsI18RIHAWVCBoCXVZnrw==", + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.17.1" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.17.1.tgz", + "integrity": "sha512-fvi1WT8aSiGAKrcTw8Qg3RYgcwW8GZMHcqEm4AyDBEy72JZlFBSY80cTQ75MslINjCHXLDT+9EN8AGI9WVY7uA==", + "license": "MIT" + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.17.1.tgz", + "integrity": "sha512-NbBt6eBWlsXc5geSpfPRC5dkIB/0Ptthw8r0yM5Z7D3sPlYdnTZSO9y9XWXIptRMwmZe4cM8iBMN8y0tzbcBkA==", + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.17.1" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.17.1.tgz", + "integrity": "sha512-3rL/6ofJvyL+q8TiWM3qoM9tig+SY4gB1Vbsj+UeJPnJm8Khm+7OS+r+mFraqR6pTehYqN8yGYoE7x4diEn4aA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.17.1", + "@algolia/client-search": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-common": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.17.1.tgz", + "integrity": "sha512-+r7kg4EgbFnGsDnoGSVNtXZO8xvZ0vzf1WAOV7sqV9PMf1bp6cpJP/3IuPrSk4t5w2KVl+pC8jfTM7HcFlfBEQ==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-search": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.17.1.tgz", + "integrity": "sha512-Q5YfT5gVkx60PZDQBqp/zH9aUbBdC7HVvxupiHUgnCKqRQsRZjOhLest7AI6FahepuZLBZS62COrO7v+JvKY7w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.17.1", + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.17.1.tgz", + "integrity": "sha512-Bepr2w249vODqeBtM7i++tPmUsQ9B81aupUGbDWmjA/FX+jzQqOdhW8w1CFO5kWViNKTbz2WBIJ9U3x8hOa4bA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.17.1", + "@algolia/client-search": "4.17.1", + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-analytics/node_modules/@algolia/client-common": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.17.1.tgz", + "integrity": "sha512-+r7kg4EgbFnGsDnoGSVNtXZO8xvZ0vzf1WAOV7sqV9PMf1bp6cpJP/3IuPrSk4t5w2KVl+pC8jfTM7HcFlfBEQ==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-analytics/node_modules/@algolia/client-search": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.17.1.tgz", + "integrity": "sha512-Q5YfT5gVkx60PZDQBqp/zH9aUbBdC7HVvxupiHUgnCKqRQsRZjOhLest7AI6FahepuZLBZS62COrO7v+JvKY7w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.17.1", + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.18.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.17.1.tgz", + "integrity": "sha512-gJku9DG/THJpfsSlG/az0a3QIn+VVff9kKh8PG8+7ZfxOHS+C+Y5YSeZVsC+c2cfoKLPo3CuHIiJ/p86erR3bA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.17.1", + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-personalization/node_modules/@algolia/client-common": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.17.1.tgz", + "integrity": "sha512-+r7kg4EgbFnGsDnoGSVNtXZO8xvZ0vzf1WAOV7sqV9PMf1bp6cpJP/3IuPrSk4t5w2KVl+pC8jfTM7HcFlfBEQ==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.18.0", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/logger-common": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.17.1.tgz", + "integrity": "sha512-Us28Ot+fLEmX9M96sa65VZ8EyEEzhYPxfhV9aQyKDjfXbUdJlJxKt6wZpoEg9RAPSdO8IjK9nmuW2P8au3rRsg==", + "license": "MIT" + }, + "node_modules/@algolia/logger-console": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.17.1.tgz", + "integrity": "sha512-iKGQTpOjHiE64W3JIOu6dmDvn+AfYIElI9jf/Nt6umRPmP/JI9rK+OHUoW4pKrBtdG0DPd62ppeNXzSnLxY6/g==", + "license": "MIT", + "dependencies": { + "@algolia/logger-common": "4.17.1" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.18.0", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.17.1.tgz", + "integrity": "sha512-HggXdjvVFQR0I5l7hM5WdHgQ1tqcRWeyXZz8apQ7zPWZhirmY2E9D6LVhDh/UnWQNEm7nBtM+eMFONJ3bZccIQ==", + "license": "MIT" + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.18.0", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.18.0", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.17.1.tgz", + "integrity": "sha512-ZM+qhX47Vh46mWH8/U9ihvy98HdTYpYQDSlqBD7IbiUbbyoCMke+qmdSX2MGhR2FCcXBSxejsJKKVAfbpaLVgg==", + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.17.1", + "@algolia/logger-common": "4.17.1", + "@algolia/requester-common": "4.17.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.3.tgz", + "integrity": "sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.1.tgz", + "integrity": "sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.22.0", + "@babel/helper-compilation-targets": "^7.22.1", + "@babel/helper-module-transforms": "^7.22.1", + "@babel/helpers": "^7.22.0", + "@babel/parser": "^7.22.0", + "@babel/template": "^7.21.9", + "@babel/traverse": "^7.22.1", + "@babel/types": "^7.22.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.3.tgz", + "integrity": "sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.3.tgz", + "integrity": "sha512-ahEoxgqNoYXm0k22TvOke48i1PkavGu0qGCmcq9ugi6gnmvKNaMjKBSrZTnWUi1CFEeNAUiVba0Wtzm03aSkJg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.1.tgz", + "integrity": "sha512-Rqx13UM3yVB5q0D/KwQ8+SPfX/+Rnsy1Lw1k/UwOC4KC6qrzIQoY3lYnBu5EHKBlEHHcj0M0W8ltPSkD8rqfsQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.0", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.1.tgz", + "integrity": "sha512-SowrZ9BWzYFgzUMwUmowbPSGu6CXL5MSuuCkG3bejahSpSymioPmuLdhPxNOc9MjuNGjy7M/HaXvJ8G82Lywlw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.22.1", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-member-expression-to-functions": "^7.22.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.22.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.1.tgz", + "integrity": "sha512-WWjdnfR3LPIe+0EY8td7WmjhytxXtjKAEpnAxun/hkNiyOaPlvGK+NZaBFIdi9ndYV3Gav7BpFvtUwnaJlwi1w==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", + "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.1.tgz", + "integrity": "sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.3.tgz", + "integrity": "sha512-Gl7sK04b/2WOb6OPVeNy9eFKeD3L6++CzL3ykPOWqTn08xgYYK0wz4TUh2feIImDXxcVW3/9WQ1NMKY66/jfZA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.1.tgz", + "integrity": "sha512-dxAe9E7ySDGbQdCVOY/4+UcD8M9ZFqZcZhSPsPacvCG4M+9lwtDDQfI2EoaSvmf7W/8yCBkGU0m7Pvt1ru3UZw==", + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.1", + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-simple-access": "^7.21.5", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.21.9", + "@babel/traverse": "^7.22.1", + "@babel/types": "^7.22.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", + "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.1.tgz", + "integrity": "sha512-ut4qrkE4AuSfrwHSps51ekR1ZY/ygrP1tp0WFm8oVq6nzc/hvfV/22JylndIbsf2U2M9LOMwiSddr6y+78j+OQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.1", + "@babel/helper-member-expression-to-functions": "^7.22.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.21.9", + "@babel/traverse": "^7.22.1", + "@babel/types": "^7.22.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", + "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.3.tgz", + "integrity": "sha512-jBJ7jWblbgr7r6wYZHMdIqKc73ycaTcCaWRq4/2LpuPHcx7xMlZvpGQkOYc9HeSjn6rcx15CPlgVcBtZ4WZJ2w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.21.9", + "@babel/traverse": "^7.22.1", + "@babel/types": "^7.22.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz", + "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.3.tgz", + "integrity": "sha512-6r4yRwEnorYByILoDRnEqxtojYKuiIv9FojW2E8GUKo9eWBwbKcd9IiZOZpdyXc64RmyGGyPu3/uAcrz/dq2kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-transform-optional-chaining": "^7.22.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", + "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.3.tgz", + "integrity": "sha512-i35jZJv6aO7hxEbIWQ41adVfOzjm9dcYDNeWlBMd8p0ZQRtNUCBrmGwZt+H5lb+oOC9a3svp956KP0oWGA1YsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", + "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", + "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz", + "integrity": "sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.3.tgz", + "integrity": "sha512-36A4Aq48t66btydbZd5Fk0/xJqbpg/v4QWI4AH4cYHBXy9Mu42UOupZpebKFiCFNT9S9rJFcsld0gsv0ayLjtA==", + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", + "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.3.tgz", + "integrity": "sha512-mASLsd6rhOrLZ5F3WbCxkzl67mmOnqik0zrg5W6D/X0QMW7HtvnoL1dRARLKIbMP3vXwkwziuLesPqWVGIl6Bw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.3.tgz", + "integrity": "sha512-5BirgNWNOx7cwbTJCOmKFJ1pZjwk5MUfMIwiBBvsirCJMZeQgs5pk6i1OlkVg+1Vef5LfBahFOrdCnAWvkVKMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", + "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz", + "integrity": "sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/template": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", + "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.1.tgz", + "integrity": "sha512-rlhWtONnVBPdmt+jeewS0qSnMz/3yLFrqAP8hHC6EDcrYRSyuz9f9yQhHvVn2Ad6+yO9fHXac5piudeYrInxwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "license": "MIT", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.3.tgz", + "integrity": "sha512-5Ti1cHLTDnt3vX61P9KZ5IG09bFXp4cDVFJIAeCZuxu9OXXJJZp5iP0n/rzM2+iAutJY+KWEyyHcRaHlpQ/P5g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz", + "integrity": "sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.3.tgz", + "integrity": "sha512-IuvOMdeOOY2X4hRNAT6kwbePtK21BUyrAEgLKviL8pL6AEEVUVcqtRdN/HJXBLGIbt9T3ETmXRnFedRRmQNTYw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.3.tgz", + "integrity": "sha512-CbayIfOw4av2v/HYZEsH+Klks3NC2/MFIR3QR8gnpGNNPEaq2fdlVCRYG/paKs7/5hvBLQ+H70pGWOHtlNEWNA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz", + "integrity": "sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.21.5", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-simple-access": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.3.tgz", + "integrity": "sha512-V21W3bKLxO3ZjcBJZ8biSvo5gQ85uIXW2vJfh7JSWf/4SLUSr1tOoHX3ruN4+Oqa2m+BKfsxTR1I+PsvkIWvNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-validator-identifier": "^7.19.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.3.tgz", + "integrity": "sha512-c6HrD/LpUdNNJsISQZpds3TXvfYIAbo+efE9aWmY/PmSRD0agrJ9cPMt4BmArwUQ7ZymEWTFjTyp+yReLJZh0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.3.tgz", + "integrity": "sha512-5RuJdSo89wKdkRTqtM9RVVJzHum9c2s0te9rB7vZC1zKKxcioWIy+xcu4OoIAjyFZhb/bp5KkunuLin1q7Ct+w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.3.tgz", + "integrity": "sha512-CpaoNp16nX7ROtLONNuCyenYdY/l7ZsR6aoVa7rW7nMWisoNoQNIH5Iay/4LDyRjKMuElMqXiBoOQCDLTMGZiw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.3.tgz", + "integrity": "sha512-+AF88fPDJrnseMh5vD9+SH6wq4ZMvpiTMHh58uLs+giMEyASFVhcT3NkoyO+NebFCNnpHJEq5AXO2txV4AGPDQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.3.tgz", + "integrity": "sha512-38bzTsqMMCI46/TQnJwPPpy33EjLCc1Gsm2hRTF6zTMWnKsN61vdrpuzIEGQyKEhDSYDKyZHrrd5FMj4gcUHhw==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.3", + "@babel/helper-compilation-targets": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.3.tgz", + "integrity": "sha512-bnDFWXFzWY0BsOyqaoSXvMQ2F35zutQipugog/rqotL2S4ciFOKlRYUu9djt4iq09oh2/34hqfRR2k1dIvuu4g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.3.tgz", + "integrity": "sha512-63v3/UFFxhPKT8j8u1jTTGVyITxl7/7AfOqK8C5gz1rHURPUGe3y5mvIf68eYKGoBNahtJnTxBKug4BQOnzeJg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.3.tgz", + "integrity": "sha512-x7QHQJHPuD9VmfpzboyGJ5aHEr9r7DsAsdxdhJiTB3J3j8dyl+NFZ+rX5Q2RWFDCs61c06qBfS4ys2QYn8UkMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.3.tgz", + "integrity": "sha512-fC7jtjBPFqhqpPAE+O4LKwnLq7gGkD3ZmC2E3i4qWH34mH3gOg2Xrq5YMHUq6DM30xhqM1DNftiRaSqVjEG+ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.3.tgz", + "integrity": "sha512-C7MMl4qWLpgVCbXfj3UW8rR1xeCnisQ0cU7YJHV//8oNBS0aCIVg1vFnZXxOckHhEpQyqNNkWmvSEWnMLlc+Vw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.3.tgz", + "integrity": "sha512-b5J6muxQYp4H7loAQv/c7GO5cPuRA6H5hx4gO+/Hn+Cu9MRQU0PNiUoWq1L//8sq6kFSNxGXFb2XTaUfa9y+Pg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", + "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.3.tgz", + "integrity": "sha512-JEulRWG2f04a7L8VWaOngWiK6p+JOSpB+DAtwfJgOaej1qdbNxqtK7MwTBHjUA10NeFcszlFNqCdbRcirzh2uQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-jsx": "^7.21.4", + "@babel/types": "^7.22.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", + "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", + "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", + "integrity": "sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.4.tgz", + "integrity": "sha512-Urkiz1m4zqiRo17klj+l3nXgiRTFQng91Bc1eiLF7BMQu1e7wE5Gcq9xSv062IF068NHjcutSbIMev60gXxAvA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-plugin-utils": "^7.21.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.3.tgz", + "integrity": "sha512-pyjnCIniO5PNaEuGxT28h0HbMru3qCVrMqVgVOz/krComdIrY9W6FCLBq9NWHY8HDGaUlan+UhmZElDENIfCcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/plugin-syntax-typescript": "^7.21.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz", + "integrity": "sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.3.tgz", + "integrity": "sha512-5ScJ+OmdX+O6HRuMGW4kv7RL9vIKdtdAj9wuWUKy1wbHY3jaM/UlyIiC1G7J6UJiiyMukjjK0QwL3P0vBd0yYg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.3.tgz", + "integrity": "sha512-hNufLdkF8vqywRp+P55j4FHXqAX2LRUccoZHH7AFn1pq5ZOO2ISKW9w13bFZVjBoTqeve2HOgoJCcaziJVhGNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.4.tgz", + "integrity": "sha512-c3lHOjbwBv0TkhYCr+XCR6wKcSZ1QbQTVdSkZUaVpLv8CVWotBMArWUi5UAJrcrQaEnleVkkvaV8F/pmc/STZQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.3", + "@babel/helper-compilation-targets": "^7.22.1", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.3", + "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-import-attributes": "^7.22.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.21.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.3", + "@babel/plugin-transform-async-to-generator": "^7.20.7", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.21.0", + "@babel/plugin-transform-class-properties": "^7.22.3", + "@babel/plugin-transform-class-static-block": "^7.22.3", + "@babel/plugin-transform-classes": "^7.21.0", + "@babel/plugin-transform-computed-properties": "^7.21.5", + "@babel/plugin-transform-destructuring": "^7.21.3", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-dynamic-import": "^7.22.1", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-export-namespace-from": "^7.22.3", + "@babel/plugin-transform-for-of": "^7.21.5", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-json-strings": "^7.22.3", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.3", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.20.11", + "@babel/plugin-transform-modules-commonjs": "^7.21.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.3", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.3", + "@babel/plugin-transform-new-target": "^7.22.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.3", + "@babel/plugin-transform-numeric-separator": "^7.22.3", + "@babel/plugin-transform-object-rest-spread": "^7.22.3", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-optional-catch-binding": "^7.22.3", + "@babel/plugin-transform-optional-chaining": "^7.22.3", + "@babel/plugin-transform-parameters": "^7.22.3", + "@babel/plugin-transform-private-methods": "^7.22.3", + "@babel/plugin-transform-private-property-in-object": "^7.22.3", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.21.5", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.20.7", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.21.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.3", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.3", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.4", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "core-js-compat": "^3.30.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.3.tgz", + "integrity": "sha512-lxDz1mnZ9polqClBCVBjIVUypoB4qV3/tZUDb/IlYbW1kiiLaXaX+bInbRjl+lNQ/iUZraQ3+S8daEmoELMWug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-transform-react-display-name": "^7.18.6", + "@babel/plugin-transform-react-jsx": "^7.22.3", + "@babel/plugin-transform-react-jsx-development": "^7.18.6", + "@babel/plugin-transform-react-pure-annotations": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz", + "integrity": "sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-syntax-jsx": "^7.21.4", + "@babel/plugin-transform-modules-commonjs": "^7.21.5", + "@babel/plugin-transform-typescript": "^7.21.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "license": "MIT" + }, + "node_modules/@babel/runtime": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", + "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.22.3.tgz", + "integrity": "sha512-6bdmknScYKmt8I9VjsJuKKGr+TwUb555FTf6tT1P/ANlCjTHCiYLhiQ4X/O7J731w5NOqu8c1aYHEVuOwPz7jA==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.21.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.21.9.tgz", + "integrity": "sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.21.4", + "@babel/parser": "^7.21.9", + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.4.tgz", + "integrity": "sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.22.3", + "@babel/helper-environment-visitor": "^7.22.1", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.22.4", + "@babel/types": "^7.22.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.4.tgz", + "integrity": "sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.21.5", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.4.0.tgz", + "integrity": "sha512-Hg8Xfma+rFwRi6Y/pfei4FJoQ1hdVURmmNs/XPoMTCPAImU+d5yxj+M+qdLtNjWRpfWziU4dQcqY94xgFBn2dg==", + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.4.0.tgz", + "integrity": "sha512-ufrp5879XYGojgS30ZAp8H4qIMbahRHB9M85VDBP36Xgz5QjYM54i1URKj5e219F7gqTtOivfztFTij6itc0MQ==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.8.2", + "@algolia/autocomplete-preset-algolia": "1.8.2", + "@docsearch/css": "3.4.0", + "algoliasearch": "^4.0.0" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/core/-/core-2.4.3.tgz", + "integrity": "sha512-dWH5P7cgeNSIg9ufReX6gaCl/TmrGKD38Orbwuz05WPhAQtFXHd5B8Qym1TiXfvUNvwoYKkAJOJuGe8ou0Z7PA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.6", + "@babel/generator": "^7.18.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.18.6", + "@babel/preset-env": "^7.18.6", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.18.6", + "@babel/runtime": "^7.18.6", + "@babel/runtime-corejs3": "^7.18.6", + "@babel/traverse": "^7.18.8", + "@docusaurus/cssnano-preset": "2.4.3", + "@docusaurus/logger": "2.4.3", + "@docusaurus/mdx-loader": "2.4.3", + "@docusaurus/react-loadable": "5.5.2", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-common": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "@slorber/static-site-generator-webpack-plugin": "^4.0.7", + "@svgr/webpack": "^6.2.1", + "autoprefixer": "^10.4.7", + "babel-loader": "^8.2.5", + "babel-plugin-dynamic-import-node": "^2.3.3", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "clean-css": "^5.3.0", + "cli-table3": "^0.6.2", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "copy-webpack-plugin": "^11.0.0", + "core-js": "^3.23.3", + "css-loader": "^6.7.1", + "css-minimizer-webpack-plugin": "^4.0.0", + "cssnano": "^5.1.12", + "del": "^6.1.1", + "detect-port": "^1.3.0", + "escape-html": "^1.0.3", + "eta": "^2.0.0", + "file-loader": "^6.2.0", + "fs-extra": "^10.1.0", + "html-minifier-terser": "^6.1.0", + "html-tags": "^3.2.0", + "html-webpack-plugin": "^5.5.0", + "import-fresh": "^3.3.0", + "leven": "^3.1.0", + "lodash": "^4.17.21", + "mini-css-extract-plugin": "^2.6.1", + "postcss": "^8.4.14", + "postcss-loader": "^7.0.0", + "prompts": "^2.4.2", + "react-dev-utils": "^12.0.1", + "react-helmet-async": "^1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@5.5.2", + "react-loadable-ssr-addon-v5-slorber": "^1.0.1", + "react-router": "^5.3.3", + "react-router-config": "^5.1.1", + "react-router-dom": "^5.3.3", + "rtl-detect": "^1.0.4", + "semver": "^7.3.7", + "serve-handler": "^6.1.3", + "shelljs": "^0.8.5", + "terser-webpack-plugin": "^5.3.3", + "tslib": "^2.4.0", + "update-notifier": "^5.1.0", + "url-loader": "^4.1.1", + "wait-on": "^6.0.1", + "webpack": "^5.73.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-dev-server": "^4.9.3", + "webpack-merge": "^5.8.0", + "webpackbar": "^5.0.2" + }, + "bin": { + "docusaurus": "bin/docusaurus.mjs" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/cssnano-preset": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.4.3.tgz", + "integrity": "sha512-ZvGSRCi7z9wLnZrXNPG6DmVPHdKGd8dIn9pYbEOFiYihfv4uDR3UtxogmKf+rT8ZlKFf5Lqne8E8nt08zNM8CA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-advanced": "^5.3.8", + "postcss": "^8.4.14", + "postcss-sort-media-queries": "^4.2.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + } + }, + "node_modules/@docusaurus/logger": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/logger/-/logger-2.4.3.tgz", + "integrity": "sha512-Zxws7r3yLufk9xM1zq9ged0YHs65mlRmtsobnFkdZTxWXdTYlWWLWdKyNKAsVC+D7zg+pv2fGbyabdOnyZOM3w==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + } + }, + "node_modules/@docusaurus/mdx-loader": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/mdx-loader/-/mdx-loader-2.4.3.tgz", + "integrity": "sha512-b1+fDnWtl3GiqkL0BRjYtc94FZrcDDBV1j8446+4tptB9BAOlePwG2p/pK6vGvfL53lkOsszXMghr2g67M0vCw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.18.8", + "@babel/traverse": "^7.18.8", + "@docusaurus/logger": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@mdx-js/mdx": "^1.6.22", + "escape-html": "^1.0.3", + "file-loader": "^6.2.0", + "fs-extra": "^10.1.0", + "image-size": "^1.0.1", + "mdast-util-to-string": "^2.0.0", + "remark-emoji": "^2.2.0", + "stringify-object": "^3.3.0", + "tslib": "^2.4.0", + "unified": "^9.2.2", + "unist-util-visit": "^2.0.3", + "url-loader": "^4.1.1", + "webpack": "^5.73.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.4.3.tgz", + "integrity": "sha512-cwkBkt1UCiduuvEAo7XZY01dJfRn7UR/75mBgOdb1hKknhrabJZ8YH+7savd/y9kLExPyrhe0QwdS9GuzsRRIA==", + "license": "MIT", + "dependencies": { + "@docusaurus/react-loadable": "5.5.2", + "@docusaurus/types": "2.4.3", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "*", + "react-loadable": "npm:@docusaurus/react-loadable@5.5.2" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@docusaurus/plugin-content-blog": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.4.3.tgz", + "integrity": "sha512-PVhypqaA0t98zVDpOeTqWUTvRqCEjJubtfFUQ7zJNYdbYTbS/E/ytq6zbLVsN/dImvemtO/5JQgjLxsh8XLo8Q==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/logger": "2.4.3", + "@docusaurus/mdx-loader": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-common": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "cheerio": "^1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^10.1.0", + "lodash": "^4.17.21", + "reading-time": "^1.5.0", + "tslib": "^2.4.0", + "unist-util-visit": "^2.0.3", + "utility-types": "^3.10.0", + "webpack": "^5.73.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-docs": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.4.3.tgz", + "integrity": "sha512-N7Po2LSH6UejQhzTCsvuX5NOzlC+HiXOVvofnEPj0WhMu1etpLEXE6a4aTxrtg95lQ5kf0xUIdjX9sh3d3G76A==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/logger": "2.4.3", + "@docusaurus/mdx-loader": "2.4.3", + "@docusaurus/module-type-aliases": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "@types/react-router-config": "^5.0.6", + "combine-promises": "^1.1.0", + "fs-extra": "^10.1.0", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "tslib": "^2.4.0", + "utility-types": "^3.10.0", + "webpack": "^5.73.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-pages": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.4.3.tgz", + "integrity": "sha512-txtDVz7y3zGk67q0HjG0gRttVPodkHqE0bpJ+7dOaTH40CQFLSh7+aBeGnPOTl+oCPG+hxkim4SndqPqXjQ8Bg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/mdx-loader": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "fs-extra": "^10.1.0", + "tslib": "^2.4.0", + "webpack": "^5.73.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-debug/-/plugin-debug-2.4.3.tgz", + "integrity": "sha512-LkUbuq3zCmINlFb+gAd4ZvYr+bPAzMC0hwND4F7V9bZ852dCX8YoWyovVUBKq4er1XsOwSQaHmNGtObtn8Av8Q==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils": "2.4.3", + "fs-extra": "^10.1.0", + "react-json-view": "^1.21.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-analytics": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.4.3.tgz", + "integrity": "sha512-KzBV3k8lDkWOhg/oYGxlK5o9bOwX7KpPc/FTWoB+SfKhlHfhq7qcQdMi1elAaVEIop8tgK6gD1E58Q+XC6otSQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-gtag": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.4.3.tgz", + "integrity": "sha512-5FMg0rT7sDy4i9AGsvJC71MQrqQZwgLNdDetLEGDHLfSHLvJhQbTCUGbGXknUgWXQJckcV/AILYeJy+HhxeIFA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.4.3.tgz", + "integrity": "sha512-1jTzp71yDGuQiX9Bi0pVp3alArV0LSnHXempvQTxwCGAEzUWWaBg4d8pocAlTpbP9aULQQqhgzrs8hgTRPOM0A==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-sitemap": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.3.tgz", + "integrity": "sha512-LRQYrK1oH1rNfr4YvWBmRzTL0LN9UAPxBbghgeFRBm5yloF6P+zv1tm2pe2hQTX/QP5bSKdnajCvfnScgKXMZQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/logger": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-common": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "fs-extra": "^10.1.0", + "sitemap": "^7.1.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/preset-classic/-/preset-classic-2.4.3.tgz", + "integrity": "sha512-tRyMliepY11Ym6hB1rAFSNGwQDpmszvWYJvlK1E+md4SW8i6ylNHtpZjaYFff9Mdk3i/Pg8ItQq9P0daOJAvQw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/plugin-content-blog": "2.4.3", + "@docusaurus/plugin-content-docs": "2.4.3", + "@docusaurus/plugin-content-pages": "2.4.3", + "@docusaurus/plugin-debug": "2.4.3", + "@docusaurus/plugin-google-analytics": "2.4.3", + "@docusaurus/plugin-google-gtag": "2.4.3", + "@docusaurus/plugin-google-tag-manager": "2.4.3", + "@docusaurus/plugin-sitemap": "2.4.3", + "@docusaurus/theme-classic": "2.4.3", + "@docusaurus/theme-common": "2.4.3", + "@docusaurus/theme-search-algolia": "2.4.3", + "@docusaurus/types": "2.4.3" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/react-loadable": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz", + "integrity": "sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@docusaurus/theme-classic": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/theme-classic/-/theme-classic-2.4.3.tgz", + "integrity": "sha512-QKRAJPSGPfDY2yCiPMIVyr+MqwZCIV2lxNzqbyUW0YkrlmdzzP3WuQJPMGLCjWgQp/5c9kpWMvMxjhpZx1R32Q==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "2.4.3", + "@docusaurus/mdx-loader": "2.4.3", + "@docusaurus/module-type-aliases": "2.4.3", + "@docusaurus/plugin-content-blog": "2.4.3", + "@docusaurus/plugin-content-docs": "2.4.3", + "@docusaurus/plugin-content-pages": "2.4.3", + "@docusaurus/theme-common": "2.4.3", + "@docusaurus/theme-translations": "2.4.3", + "@docusaurus/types": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-common": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "@mdx-js/react": "^1.6.22", + "clsx": "^1.2.1", + "copy-text-to-clipboard": "^3.0.1", + "infima": "0.2.0-alpha.43", + "lodash": "^4.17.21", + "nprogress": "^0.2.0", + "postcss": "^8.4.14", + "prism-react-renderer": "^1.3.5", + "prismjs": "^1.28.0", + "react-router-dom": "^5.3.3", + "rtlcss": "^3.5.0", + "tslib": "^2.4.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/theme-common/-/theme-common-2.4.3.tgz", + "integrity": "sha512-7KaDJBXKBVGXw5WOVt84FtN8czGWhM0lbyWEZXGp8AFfL6sZQfRTluFp4QriR97qwzSyOfQb+nzcDZZU4tezUw==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "2.4.3", + "@docusaurus/module-type-aliases": "2.4.3", + "@docusaurus/plugin-content-blog": "2.4.3", + "@docusaurus/plugin-content-docs": "2.4.3", + "@docusaurus/plugin-content-pages": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-common": "2.4.3", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "clsx": "^1.2.1", + "parse-numeric-range": "^1.3.0", + "prism-react-renderer": "^1.3.5", + "tslib": "^2.4.0", + "use-sync-external-store": "^1.2.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/theme-search-algolia": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz", + "integrity": "sha512-jziq4f6YVUB5hZOB85ELATwnxBz/RmSLD3ksGQOLDPKVzat4pmI8tddNWtriPpxR04BNT+ZfpPUMFkNFetSW1Q==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "^3.1.1", + "@docusaurus/core": "2.4.3", + "@docusaurus/logger": "2.4.3", + "@docusaurus/plugin-content-docs": "2.4.3", + "@docusaurus/theme-common": "2.4.3", + "@docusaurus/theme-translations": "2.4.3", + "@docusaurus/utils": "2.4.3", + "@docusaurus/utils-validation": "2.4.3", + "algoliasearch": "^4.13.1", + "algoliasearch-helper": "^3.10.0", + "clsx": "^1.2.1", + "eta": "^2.0.0", + "fs-extra": "^10.1.0", + "lodash": "^4.17.21", + "tslib": "^2.4.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/theme-translations": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz", + "integrity": "sha512-H4D+lbZbjbKNS/Zw1Lel64PioUAIT3cLYYJLUf3KkuO/oc9e0QCVhIYVtUI2SfBCF2NNdlyhBDQEEMygsCedIg==", + "license": "MIT", + "dependencies": { + "fs-extra": "^10.1.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + } + }, + "node_modules/@docusaurus/types": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/types/-/types-2.4.3.tgz", + "integrity": "sha512-W6zNLGQqfrp/EoPD0bhb9n7OobP+RHpmvVzpA+Z/IuU3Q63njJM24hmT0GYboovWcDtFmnIJC9wcyx4RVPQscw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.6.0", + "react-helmet-async": "^1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.73.0", + "webpack-merge": "^5.8.0" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/utils": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/utils/-/utils-2.4.3.tgz", + "integrity": "sha512-fKcXsjrD86Smxv8Pt0TBFqYieZZCPh4cbf9oszUq/AMhZn3ujwpKaVYZACPX8mmjtYx0JOgNx52CREBfiGQB4A==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "2.4.3", + "@svgr/webpack": "^6.2.1", + "escape-string-regexp": "^4.0.0", + "file-loader": "^6.2.0", + "fs-extra": "^10.1.0", + "github-slugger": "^1.4.0", + "globby": "^11.1.0", + "gray-matter": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.5", + "resolve-pathname": "^3.0.0", + "shelljs": "^0.8.5", + "tslib": "^2.4.0", + "url-loader": "^4.1.1", + "webpack": "^5.73.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "@docusaurus/types": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/types": { + "optional": true + } + } + }, + "node_modules/@docusaurus/utils-common": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/utils-common/-/utils-common-2.4.3.tgz", + "integrity": "sha512-/jascp4GbLQCPVmcGkPzEQjNaAk3ADVfMtudk49Ggb+131B1WDD6HqlSmDf8MxGdy7Dja2gc+StHf01kiWoTDQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "@docusaurus/types": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/types": { + "optional": true + } + } + }, + "node_modules/@docusaurus/utils-validation": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/@docusaurus/utils-validation/-/utils-validation-2.4.3.tgz", + "integrity": "sha512-G2+Vt3WR5E/9drAobP+hhZQMaswRwDlp6qOMi7o7ZypB+VO7N//DZWhZEwhcRGepMDJGQEwtPv7UxtYwPL9PBw==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "2.4.3", + "@docusaurus/utils": "2.4.3", + "joi": "^17.6.0", + "js-yaml": "^4.1.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@headlessui/react": { + "version": "1.7.17", + "resolved": "https://registry.npmmirror.com/@headlessui/react/-/react-1.7.17.tgz", + "integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==", + "license": "MIT", + "dependencies": { + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "license": "MIT" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "1.6.22", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz", + "integrity": "sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==", + "license": "MIT", + "dependencies": { + "@babel/core": "7.12.9", + "@babel/plugin-syntax-jsx": "7.12.1", + "@babel/plugin-syntax-object-rest-spread": "7.8.3", + "@mdx-js/util": "1.6.22", + "babel-plugin-apply-mdx-type-prop": "1.6.22", + "babel-plugin-extract-import-names": "1.6.22", + "camelcase-css": "2.0.1", + "detab": "2.0.4", + "hast-util-raw": "6.0.1", + "lodash.uniq": "4.5.0", + "mdast-util-to-hast": "10.0.1", + "remark-footnotes": "2.0.0", + "remark-mdx": "1.6.22", + "remark-parse": "8.0.3", + "remark-squeeze-paragraphs": "4.0.0", + "style-to-object": "0.3.0", + "unified": "9.2.0", + "unist-builder": "2.0.3", + "unist-util-visit": "2.0.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/mdx/node_modules/@babel/core": { + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", + "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.9", + "@babel/types": "^7.12.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@mdx-js/mdx/node_modules/@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@mdx-js/mdx/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@mdx-js/mdx/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@mdx-js/mdx/node_modules/unified": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", + "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==", + "license": "MIT", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "1.6.22", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-1.6.22.tgz", + "integrity": "sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + } + }, + "node_modules/@mdx-js/util": { + "version": "1.6.22", + "resolved": "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz", + "integrity": "sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@slorber/static-site-generator-webpack-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz", + "integrity": "sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==", + "license": "MIT", + "dependencies": { + "eval": "^0.1.8", + "p-map": "^4.0.0", + "webpack-sources": "^3.2.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz", + "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz", + "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz", + "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz", + "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz", + "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz", + "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz", + "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1", + "@svgr/babel-plugin-remove-jsx-attribute": "*", + "@svgr/babel-plugin-remove-jsx-empty-expression": "*", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1", + "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1", + "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1", + "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1", + "@svgr/babel-plugin-transform-svg-component": "^6.5.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz", + "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/plugin-jsx": "^6.5.1", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz", + "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.0", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz", + "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/hast-util-to-babel-ast": "^6.5.1", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "^6.0.0" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz", + "integrity": "sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "svgo": "^2.8.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-6.5.1.tgz", + "integrity": "sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.19.6", + "@babel/plugin-transform-react-constant-elements": "^7.18.12", + "@babel/preset-env": "^7.19.4", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.18.6", + "@svgr/core": "^6.5.1", + "@svgr/plugin-jsx": "^6.5.1", + "@svgr/plugin-svgo": "^6.5.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@tsconfig/docusaurus": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-1.0.7.tgz", + "integrity": "sha512-ffTXxGIP/IRMCjuzHd6M4/HdIrw1bMfC7Bv8hMkTadnePkpe0lG0oDSdbRpSDZb2rQMAgpbWiR10BvxvNYwYrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", + "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.2.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", + "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==", + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "license": "MIT" + }, + "node_modules/@types/parse5": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", + "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.7.tgz", + "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-config": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.7.tgz", + "integrity": "sha512-pFFVXUIydHlcJP6wJm7sDii5mD/bCmmAY0wQzq+M+uX7bqS95AQqHZWP1iNMKrWVQSuHIzj5qi9BvrtLX2/T4w==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "^5.1.0" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "license": "MIT", + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/algoliasearch": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.17.1.tgz", + "integrity": "sha512-4GDQ1RhP2qUR3x8PevFRbEdqZqIARNViZYjgTJmA1T7wRNtFA3W4Aqc/RsODqa1J8IO/QDla5x4tWuUS8NV8wA==", + "license": "MIT", + "dependencies": { + "@algolia/cache-browser-local-storage": "4.17.1", + "@algolia/cache-common": "4.17.1", + "@algolia/cache-in-memory": "4.17.1", + "@algolia/client-account": "4.17.1", + "@algolia/client-analytics": "4.17.1", + "@algolia/client-common": "4.17.1", + "@algolia/client-personalization": "4.17.1", + "@algolia/client-search": "4.17.1", + "@algolia/logger-common": "4.17.1", + "@algolia/logger-console": "4.17.1", + "@algolia/requester-browser-xhr": "4.17.1", + "@algolia/requester-common": "4.17.1", + "@algolia/requester-node-http": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.13.0.tgz", + "integrity": "sha512-kV3c1jMQCvkARtGsSDvAwuht4PAMSsQILqPiH4WFiARoa3jXJ/r1TQoBWAjWyWF48rsNYCv7kzxgB4LTxrvvuw==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/client-common": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.17.1.tgz", + "integrity": "sha512-+r7kg4EgbFnGsDnoGSVNtXZO8xvZ0vzf1WAOV7sqV9PMf1bp6cpJP/3IuPrSk4t5w2KVl+pC8jfTM7HcFlfBEQ==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/client-search": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.17.1.tgz", + "integrity": "sha512-Q5YfT5gVkx60PZDQBqp/zH9aUbBdC7HVvxupiHUgnCKqRQsRZjOhLest7AI6FahepuZLBZS62COrO7v+JvKY7w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.17.1", + "@algolia/requester-common": "4.17.1", + "@algolia/transporter": "4.17.1" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/requester-browser-xhr": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.17.1.tgz", + "integrity": "sha512-W5mGfGDsyfVR+r4pUFrYLGBEM18gs38+GNt5PE5uPULy4uVTSnnVSkJkWeRkmLBk9zEZ/Nld8m4zavK6dtEuYg==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.17.1" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/requester-node-http": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.17.1.tgz", + "integrity": "sha512-NzFWecXT6d0PPsQY9L+/qoK2deF74OLcpvqCH+Vh3mh+QzPsFafcBExdguAjZsAWDn1R6JEeFW7/fo/p0SE57w==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.17.1" + } + }, + "node_modules/animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==", + "license": "MIT" + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.7" + } + }, + "node_modules/babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "license": "MIT", + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-plugin-apply-mdx-type-prop": { + "version": "1.6.22", + "resolved": "https://registry.npmjs.org/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz", + "integrity": "sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.10.4", + "@mdx-js/util": "1.6.22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@babel/core": "^7.11.6" + } + }, + "node_modules/babel-plugin-apply-mdx-type-prop/node_modules/@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "license": "MIT" + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-extract-import-names": { + "version": "1.6.22", + "resolved": "https://registry.npmjs.org/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz", + "integrity": "sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.10.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/babel-plugin-extract-import-names/node_modules/@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "license": "MIT" + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", + "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.4.0", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", + "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0", + "core-js-compat": "^3.30.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", + "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-prismjs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-prismjs/-/babel-plugin-prismjs-2.1.0.tgz", + "integrity": "sha512-ehzSKYfeAz4U78zi/sfwsjDPlq0LvDKxNefcZTJ/iKBu+plsHsLqZhUeGf1+82LAcA35UZGbU6ksEx2Utphc/g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prismjs": "^1.18.0" + } + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base16": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", + "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==", + "license": "MIT" + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "license": "MIT", + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001558", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz", + "integrity": "sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", + "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", + "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combine-promises": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.1.0.tgz", + "integrity": "sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compressible/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/copy-text-to-clipboard": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.1.0.tgz", + "integrity": "sha512-PFM6BnjLnOON/lB3ta/Jg7Ywsv+l9kQGD4TWDCSlRBGmqnnTM5MrDkhAFgw+8HZt0wW6Q2BBE4cmy9sq+s9Qng==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", + "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", + "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", + "integrity": "sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.30.2.tgz", + "integrity": "sha512-p/npFUJXXBkCCTIlEGBdghofn00jWG6ZOtdoIXSJmAu2QBvN0IqpZXWweOytcwE6cfx8ZvVUy1vw8zxhe4Y2vg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.11" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", + "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz", + "integrity": "sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==", + "license": "MIT", + "dependencies": { + "cssnano": "^5.1.8", + "jest-worker": "^29.1.2", + "postcss": "^8.4.17", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.10.tgz", + "integrity": "sha512-fnYJyCS9jgMU+cmHO1rPSPf9axbQyD7iUhLO5Df6O4G+fKIOMps+ZbU0PdGFejFBBZ3Pftf18fn1eG7MAPUSWQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.12", + "cssnano-preset-default": "^5.2.14", + "postcss-discard-unused": "^5.1.0", + "postcss-merge-idents": "^5.1.1", + "postcss-reduce-idents": "^5.2.0", + "postcss-zindex": "^5.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "license": "MIT", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "license": "MIT", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "license": "MIT" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detab": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.4.tgz", + "integrity": "sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/detect-port": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", + "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "4" + }, + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" + } + }, + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/detect-port-alt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/detect-port-alt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/docusaurus-plugin-sass": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.3.tgz", + "integrity": "sha512-FbaE06K8NF8SPUYTwiG+83/jkXrwHJ/Afjqz3SUIGon6QvFwSSoKOcoxGQmUBnjTOk+deUONDx8jNWsegFJcBQ==", + "license": "MIT", + "dependencies": { + "sass-loader": "^10.1.1" + }, + "peerDependencies": { + "@docusaurus/core": "^2.0.0-beta", + "sass": "^1.30.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "license": "BSD-3-Clause" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.571", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.571.tgz", + "integrity": "sha512-Sc+VtKwKCDj3f/kLBjdyjMpNzoZsU6WuL/wFb6EH8USmHEcebxRXcRrVpOpayxd52tuey4RUDpUsw5OS5LhJqg==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-3.2.0.tgz", + "integrity": "sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz", + "integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "license": "MIT", + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fbemitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", + "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", + "license": "BSD-3-Clause", + "dependencies": { + "fbjs": "^3.0.0" + } + }, + "node_modules/fbjs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz", + "integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==", + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.30" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", + "license": "MIT" + }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz", + "integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==", + "license": "BSD-3-Clause", + "dependencies": { + "fbemitter": "^3.0.0", + "fbjs": "^3.0.1" + }, + "peerDependencies": { + "react": "^15.0.2 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=10", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "license": "MIT" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-to-hyperscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", + "integrity": "sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.3", + "comma-separated-tokens": "^1.0.0", + "property-information": "^5.3.0", + "space-separated-tokens": "^1.0.0", + "style-to-object": "^0.3.0", + "unist-util-is": "^4.0.0", + "web-namespaces": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz", + "integrity": "sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==", + "license": "MIT", + "dependencies": { + "@types/parse5": "^5.0.0", + "hastscript": "^6.0.0", + "property-information": "^5.0.0", + "vfile": "^4.0.0", + "vfile-location": "^3.2.0", + "web-namespaces": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-6.0.1.tgz", + "integrity": "sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^6.0.0", + "hast-util-to-parse5": "^6.0.0", + "html-void-elements": "^1.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^3.0.0", + "vfile": "^4.0.0", + "web-namespaces": "^1.0.0", + "xtend": "^4.0.0", + "zwitch": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/hast-util-to-parse5": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz", + "integrity": "sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==", + "license": "MIT", + "dependencies": { + "hast-to-hyperscript": "^9.0.0", + "property-information": "^5.0.0", + "web-namespaces": "^1.0.0", + "xtend": "^4.0.0", + "zwitch": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz", + "integrity": "sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz", + "integrity": "sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", + "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infima": { + "version": "0.2.0-alpha.43", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.43.tgz", + "integrity": "sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "license": "MIT" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-ci/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-whitespace-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", + "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-word-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", + "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.9.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "license": "MIT", + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.curry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", + "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.flow": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", + "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/markdown-escapes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", + "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-squeeze-paragraphs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz", + "integrity": "sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==", + "license": "MIT", + "dependencies": { + "unist-util-remove": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", + "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", + "license": "MIT", + "dependencies": { + "unist-util-visit": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz", + "integrity": "sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "mdast-util-definitions": "^4.0.0", + "mdurl": "^1.0.0", + "unist-builder": "^2.0.0", + "unist-util-generated": "^1.0.0", + "unist-util-position": "^3.0.0", + "unist-util-visit": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.1.tgz", + "integrity": "sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "license": "MIT", + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "license": "MIT", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-unused": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-5.1.0.tgz", + "integrity": "sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", + "license": "ISC", + "engines": { + "node": ">= 14" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.2.tgz", + "integrity": "sha512-c7qDlXErX6n0VT+LUsW+nwefVtTu3ORtVvK8EXuUIDcxo+b/euYqpuHlJAvePb0Af5e8uMjR/13e0lTuYifaig==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "jiti": "^1.18.2", + "klona": "^2.0.6", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/postcss-merge-idents": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-5.1.1.tgz", + "integrity": "sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "license": "MIT", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-idents": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-5.2.0.tgz", + "integrity": "sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sort-media-queries": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-4.4.1.tgz", + "integrity": "sha512-QDESFzDDGKgpiIh4GYXsSy6sek2yAwQx1JASl5AxBtU1Lq2JfKBljIPNdil989NcSKRQX1ToiaKphImtBuhXWw==", + "license": "MIT", + "dependencies": { + "sort-css-media-queries": "2.1.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.16" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-5.1.0.tgz", + "integrity": "sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A==", + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prism-react-renderer": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz", + "integrity": "sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==", + "license": "MIT", + "peerDependencies": { + "react": ">=0.14.9" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "license": "MIT", + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", + "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-base16-styling": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz", + "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==", + "license": "MIT", + "dependencies": { + "base16": "^1.0.0", + "lodash.curry": "^4.0.1", + "lodash.flow": "^3.3.0", + "pure-color": "^1.2.0" + } + }, + "node_modules/react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/react-dev-utils/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, + "node_modules/react-error-overlay": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", + "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", + "license": "MIT" + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/react-helmet-async": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-json-view": { + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz", + "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==", + "license": "MIT", + "dependencies": { + "flux": "^4.0.1", + "react-base16-styling": "^0.6.0", + "react-lifecycles-compat": "^3.0.4", + "react-textarea-autosize": "^8.3.2" + }, + "peerDependencies": { + "react": "^17.0.0 || ^16.3.0 || ^15.5.4", + "react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4" + } + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz", + "integrity": "sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-loadable-ssr-addon-v5-slorber": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", + "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.3" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" + } + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-config": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", + "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router": ">=5" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz", + "integrity": "sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reading-time": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", + "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", + "license": "MIT" + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "license": "MIT", + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", + "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "license": "MIT", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-2.2.0.tgz", + "integrity": "sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w==", + "license": "MIT", + "dependencies": { + "emoticon": "^3.2.0", + "node-emoji": "^1.10.0", + "unist-util-visit": "^2.0.3" + } + }, + "node_modules/remark-footnotes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-2.0.0.tgz", + "integrity": "sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "1.6.22", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-1.6.22.tgz", + "integrity": "sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "7.12.9", + "@babel/helper-plugin-utils": "7.10.4", + "@babel/plugin-proposal-object-rest-spread": "7.12.1", + "@babel/plugin-syntax-jsx": "7.12.1", + "@mdx-js/util": "1.6.22", + "is-alphabetical": "1.0.4", + "remark-parse": "8.0.3", + "unified": "9.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx/node_modules/@babel/core": { + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", + "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.9", + "@babel/types": "^7.12.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/remark-mdx/node_modules/@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "license": "MIT" + }, + "node_modules/remark-mdx/node_modules/@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/remark-mdx/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/remark-mdx/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remark-mdx/node_modules/unified": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", + "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==", + "license": "MIT", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.3.tgz", + "integrity": "sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==", + "license": "MIT", + "dependencies": { + "ccount": "^1.0.0", + "collapse-white-space": "^1.0.2", + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "is-word-character": "^1.0.0", + "markdown-escapes": "^1.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "^1.0.0", + "unherit": "^1.0.4", + "unist-util-remove-position": "^2.0.0", + "vfile-location": "^3.0.0", + "xtend": "^4.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-squeeze-paragraphs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz", + "integrity": "sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==", + "license": "MIT", + "dependencies": { + "mdast-squeeze-paragraphs": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "engines": { + "node": "*" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rtl-detect": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.0.4.tgz", + "integrity": "sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==", + "license": "BSD-3-Clause" + }, + "node_modules/rtlcss": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz", + "integrity": "sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==", + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.3.11", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + } + }, + "node_modules/rtlcss/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rtlcss/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rtlcss/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rtlcss/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", + "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.4.1.tgz", + "integrity": "sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==", + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "sass": "^1.3.0", + "webpack": "^4.36.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/sass-loader/node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "license": "MIT", + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "license": "MIT", + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-handler": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz", + "integrity": "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "fast-url-parser": "1.1.3", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "2.2.1", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", + "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==", + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.1.tgz", + "integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sort-css-media-queries": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz", + "integrity": "sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA==", + "license": "MIT", + "engines": { + "node": ">= 6.3.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "license": "MIT" + }, + "node_modules/state-toggle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", + "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", + "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", + "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "3.3.5", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.17.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.6.tgz", + "integrity": "sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==" + }, + "node_modules/trim-trailing-lines": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", + "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", + "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/unherit": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", + "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "license": "MIT", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unist-builder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", + "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-generated": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz", + "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", + "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-2.1.0.tgz", + "integrity": "sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==", + "license": "MIT", + "dependencies": { + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz", + "integrity": "sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==", + "license": "MIT", + "dependencies": { + "unist-util-visit": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/update-notifier/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz", + "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/wait-on": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz", + "integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==", + "license": "MIT", + "dependencies": { + "axios": "^0.25.0", + "joi": "^17.6.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.5.4" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", + "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.84.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.84.1.tgz", + "integrity": "sha512-ZP4qaZ7vVn/K8WN/p990SGATmrL1qg4heP/MrVneczYtpDGJWlrgZv55vxaV2ul885Kz+25MP2kSXkPe3LZfmg==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.14.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz", + "integrity": "sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg==", + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz", + "integrity": "sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpackbar": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", + "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.3", + "pretty-time": "^1.1.0", + "std-env": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "webpack": "3 || 4 || 5" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" + }, + "node_modules/wowjs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wowjs/-/wowjs-1.1.3.tgz", + "integrity": "sha512-HQp1gi56wYmjOYYOMZ08TnDGpT+AO21RJVa0t1NJ3jU8l3dMyP+sY7TO/lilzVp4JFjW88bBY87RnpxdpSKofA==", + "dependencies": { + "animate.css": "latest" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/website/src/pages/self-hosting/plan/index.tsx b/docs/website/src/pages/self-hosting/plan/index.tsx index 137971cf4658..6fbc995de77e 100644 --- a/docs/website/src/pages/self-hosting/plan/index.tsx +++ b/docs/website/src/pages/self-hosting/plan/index.tsx @@ -34,6 +34,7 @@ export default function Plan() { const [months, setMonths] = useState('3'); const [cpu, setCpu] = useState(8); const [memory, setMemory] = useState(16); + const [isOpen, setIsOpen] = useState(false); const price = useMemo(() => { return calculatePrice({ cpu, memory, months }); @@ -295,41 +296,56 @@ export default function Plan() {
月数
- -
- - - + {MonthMapList.find((item) => item.value === months)?.label} +
+ + + +
+ {isOpen && ( +
+ {MonthMapList.map((item) => ( +
{ + setMonths(item.value); + setIsOpen(false); + }} + > + {item.label} +
+ ))} +
+ )}
diff --git a/docs/website/yarn.lock b/docs/website/yarn.lock index 0d5f203176f6..b12fbda18eb6 100644 --- a/docs/website/yarn.lock +++ b/docs/website/yarn.lock @@ -67,6 +67,9 @@ "@algolia/requester-common" "4.17.1" "@algolia/transporter" "4.17.1" +"@algolia/client-common@5.18.0": + version "5.18.0" + "@algolia/client-personalization@4.17.1": version "4.17.1" resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.17.1.tgz" @@ -76,6 +79,14 @@ "@algolia/requester-common" "4.17.1" "@algolia/transporter" "4.17.1" +"@algolia/client-search@>= 4.9.1 < 6": + version "5.18.0" + dependencies: + "@algolia/client-common" "5.18.0" + "@algolia/requester-browser-xhr" "5.18.0" + "@algolia/requester-fetch" "5.18.0" + "@algolia/requester-node-http" "5.18.0" + "@algolia/client-search@4.17.1": version "4.17.1" resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.17.1.tgz" @@ -109,11 +120,21 @@ dependencies: "@algolia/requester-common" "4.17.1" +"@algolia/requester-browser-xhr@5.18.0": + version "5.18.0" + dependencies: + "@algolia/client-common" "5.18.0" + "@algolia/requester-common@4.17.1": version "4.17.1" resolved "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.17.1.tgz" integrity sha512-HggXdjvVFQR0I5l7hM5WdHgQ1tqcRWeyXZz8apQ7zPWZhirmY2E9D6LVhDh/UnWQNEm7nBtM+eMFONJ3bZccIQ== +"@algolia/requester-fetch@5.18.0": + version "5.18.0" + dependencies: + "@algolia/client-common" "5.18.0" + "@algolia/requester-node-http@4.17.1": version "4.17.1" resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.17.1.tgz" @@ -121,6 +142,11 @@ dependencies: "@algolia/requester-common" "4.17.1" +"@algolia/requester-node-http@5.18.0": + version "5.18.0" + dependencies: + "@algolia/client-common" "5.18.0" + "@algolia/transporter@4.17.1": version "4.17.1" resolved "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.17.1.tgz" @@ -132,7 +158,7 @@ "@alloc/quick-lru@^5.2.0": version "5.2.0" - resolved "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + resolved "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== "@ampproject/remapping@^2.2.0": @@ -155,6 +181,27 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.3.tgz" integrity sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ== +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.18.6", "@babel/core@^7.19.6", "@babel/core@^7.4.0-0": + version "7.22.1" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.22.1.tgz" + integrity sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.22.0" + "@babel/helper-compilation-targets" "^7.22.1" + "@babel/helper-module-transforms" "^7.22.1" + "@babel/helpers" "^7.22.0" + "@babel/parser" "^7.22.0" + "@babel/template" "^7.21.9" + "@babel/traverse" "^7.22.1" + "@babel/types" "^7.22.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz" @@ -177,27 +224,6 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.18.6", "@babel/core@^7.19.6": - version "7.22.1" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.22.1.tgz" - integrity sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.22.0" - "@babel/helper-compilation-targets" "^7.22.1" - "@babel/helper-module-transforms" "^7.22.1" - "@babel/helpers" "^7.22.0" - "@babel/parser" "^7.22.0" - "@babel/template" "^7.21.9" - "@babel/traverse" "^7.22.1" - "@babel/types" "^7.22.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" - "@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.22.0", "@babel/generator@^7.22.3": version "7.22.3" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.22.3.tgz" @@ -324,16 +350,16 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-plugin-utils@7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== - "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.21.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.21.5" resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz" integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== +"@babel/helper-plugin-utils@7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz" @@ -531,13 +557,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@7.12.1": - version "7.12.1" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz" - integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx@^7.21.4": version "7.21.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz" @@ -545,6 +564,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" +"@babel/plugin-syntax-jsx@7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz" + integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" @@ -566,7 +592,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-object-rest-spread@7.8.3", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": +"@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3", "@babel/plugin-syntax-object-rest-spread@7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== @@ -1265,9 +1291,9 @@ "@docsearch/css" "3.4.0" algoliasearch "^4.0.0" -"@docusaurus/core@2.4.3", "@docusaurus/core@^2.4.3": +"@docusaurus/core@^2.0.0-beta", "@docusaurus/core@^2.4.3", "@docusaurus/core@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/core/-/core-2.4.3.tgz#d86624901386fd8164ce4bff9cc7f16fde57f523" + resolved "https://registry.npmmirror.com/@docusaurus/core/-/core-2.4.3.tgz" integrity sha512-dWH5P7cgeNSIg9ufReX6gaCl/TmrGKD38Orbwuz05WPhAQtFXHd5B8Qym1TiXfvUNvwoYKkAJOJuGe8ou0Z7PA== dependencies: "@babel/core" "^7.18.6" @@ -1344,7 +1370,7 @@ "@docusaurus/cssnano-preset@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.4.3.tgz#1d7e833c41ce240fcc2812a2ac27f7b862f32de0" + resolved "https://registry.npmmirror.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.4.3.tgz" integrity sha512-ZvGSRCi7z9wLnZrXNPG6DmVPHdKGd8dIn9pYbEOFiYihfv4uDR3UtxogmKf+rT8ZlKFf5Lqne8E8nt08zNM8CA== dependencies: cssnano-preset-advanced "^5.3.8" @@ -1354,7 +1380,7 @@ "@docusaurus/logger@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/logger/-/logger-2.4.3.tgz#518bbc965fb4ebe8f1d0b14e5f4161607552d34c" + resolved "https://registry.npmmirror.com/@docusaurus/logger/-/logger-2.4.3.tgz" integrity sha512-Zxws7r3yLufk9xM1zq9ged0YHs65mlRmtsobnFkdZTxWXdTYlWWLWdKyNKAsVC+D7zg+pv2fGbyabdOnyZOM3w== dependencies: chalk "^4.1.2" @@ -1362,7 +1388,7 @@ "@docusaurus/mdx-loader@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/mdx-loader/-/mdx-loader-2.4.3.tgz#e8ff37f30a060eaa97b8121c135f74cb531a4a3e" + resolved "https://registry.npmmirror.com/@docusaurus/mdx-loader/-/mdx-loader-2.4.3.tgz" integrity sha512-b1+fDnWtl3GiqkL0BRjYtc94FZrcDDBV1j8446+4tptB9BAOlePwG2p/pK6vGvfL53lkOsszXMghr2g67M0vCw== dependencies: "@babel/parser" "^7.18.8" @@ -1383,9 +1409,9 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@docusaurus/module-type-aliases@2.4.3", "@docusaurus/module-type-aliases@^2.4.3": +"@docusaurus/module-type-aliases@^2.4.3", "@docusaurus/module-type-aliases@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.4.3.tgz#d08ef67e4151e02f352a2836bcf9ecde3b9c56ac" + resolved "https://registry.npmmirror.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.4.3.tgz" integrity sha512-cwkBkt1UCiduuvEAo7XZY01dJfRn7UR/75mBgOdb1hKknhrabJZ8YH+7savd/y9kLExPyrhe0QwdS9GuzsRRIA== dependencies: "@docusaurus/react-loadable" "5.5.2" @@ -1399,7 +1425,7 @@ "@docusaurus/plugin-content-blog@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.4.3.tgz#6473b974acab98e967414d8bbb0d37e0cedcea14" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.4.3.tgz" integrity sha512-PVhypqaA0t98zVDpOeTqWUTvRqCEjJubtfFUQ7zJNYdbYTbS/E/ytq6zbLVsN/dImvemtO/5JQgjLxsh8XLo8Q== dependencies: "@docusaurus/core" "2.4.3" @@ -1421,7 +1447,7 @@ "@docusaurus/plugin-content-docs@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.4.3.tgz#aa224c0512351e81807adf778ca59fd9cd136973" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.4.3.tgz" integrity sha512-N7Po2LSH6UejQhzTCsvuX5NOzlC+HiXOVvofnEPj0WhMu1etpLEXE6a4aTxrtg95lQ5kf0xUIdjX9sh3d3G76A== dependencies: "@docusaurus/core" "2.4.3" @@ -1443,7 +1469,7 @@ "@docusaurus/plugin-content-pages@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.4.3.tgz#7f285e718b53da8c8d0101e70840c75b9c0a1ac0" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.4.3.tgz" integrity sha512-txtDVz7y3zGk67q0HjG0gRttVPodkHqE0bpJ+7dOaTH40CQFLSh7+aBeGnPOTl+oCPG+hxkim4SndqPqXjQ8Bg== dependencies: "@docusaurus/core" "2.4.3" @@ -1457,7 +1483,7 @@ "@docusaurus/plugin-debug@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-debug/-/plugin-debug-2.4.3.tgz#2f90eb0c9286a9f225444e3a88315676fe02c245" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-debug/-/plugin-debug-2.4.3.tgz" integrity sha512-LkUbuq3zCmINlFb+gAd4ZvYr+bPAzMC0hwND4F7V9bZ852dCX8YoWyovVUBKq4er1XsOwSQaHmNGtObtn8Av8Q== dependencies: "@docusaurus/core" "2.4.3" @@ -1469,7 +1495,7 @@ "@docusaurus/plugin-google-analytics@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.4.3.tgz#0d19993136ade6f7a7741251b4f617400d92ab45" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.4.3.tgz" integrity sha512-KzBV3k8lDkWOhg/oYGxlK5o9bOwX7KpPc/FTWoB+SfKhlHfhq7qcQdMi1elAaVEIop8tgK6gD1E58Q+XC6otSQ== dependencies: "@docusaurus/core" "2.4.3" @@ -1479,7 +1505,7 @@ "@docusaurus/plugin-google-gtag@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.4.3.tgz#e1a80b0696771b488562e5b60eff21c9932d9e1c" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.4.3.tgz" integrity sha512-5FMg0rT7sDy4i9AGsvJC71MQrqQZwgLNdDetLEGDHLfSHLvJhQbTCUGbGXknUgWXQJckcV/AILYeJy+HhxeIFA== dependencies: "@docusaurus/core" "2.4.3" @@ -1489,7 +1515,7 @@ "@docusaurus/plugin-google-tag-manager@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.4.3.tgz#e41fbf79b0ffc2de1cc4013eb77798cff0ad98e3" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.4.3.tgz" integrity sha512-1jTzp71yDGuQiX9Bi0pVp3alArV0LSnHXempvQTxwCGAEzUWWaBg4d8pocAlTpbP9aULQQqhgzrs8hgTRPOM0A== dependencies: "@docusaurus/core" "2.4.3" @@ -1499,7 +1525,7 @@ "@docusaurus/plugin-sitemap@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.3.tgz#1b3930900a8f89670ce7e8f83fb4730cd3298c32" + resolved "https://registry.npmmirror.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.3.tgz" integrity sha512-LRQYrK1oH1rNfr4YvWBmRzTL0LN9UAPxBbghgeFRBm5yloF6P+zv1tm2pe2hQTX/QP5bSKdnajCvfnScgKXMZQ== dependencies: "@docusaurus/core" "2.4.3" @@ -1514,7 +1540,7 @@ "@docusaurus/preset-classic@^2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/preset-classic/-/preset-classic-2.4.3.tgz#074c57ebf29fa43d23bd1c8ce691226f542bc262" + resolved "https://registry.npmmirror.com/@docusaurus/preset-classic/-/preset-classic-2.4.3.tgz" integrity sha512-tRyMliepY11Ym6hB1rAFSNGwQDpmszvWYJvlK1E+md4SW8i6ylNHtpZjaYFff9Mdk3i/Pg8ItQq9P0daOJAvQw== dependencies: "@docusaurus/core" "2.4.3" @@ -1541,7 +1567,7 @@ "@docusaurus/theme-classic@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/theme-classic/-/theme-classic-2.4.3.tgz#29360f2eb03a0e1686eb19668633ef313970ee8f" + resolved "https://registry.npmmirror.com/@docusaurus/theme-classic/-/theme-classic-2.4.3.tgz" integrity sha512-QKRAJPSGPfDY2yCiPMIVyr+MqwZCIV2lxNzqbyUW0YkrlmdzzP3WuQJPMGLCjWgQp/5c9kpWMvMxjhpZx1R32Q== dependencies: "@docusaurus/core" "2.4.3" @@ -1572,7 +1598,7 @@ "@docusaurus/theme-common@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/theme-common/-/theme-common-2.4.3.tgz#bb31d70b6b67d0bdef9baa343192dcec49946a2e" + resolved "https://registry.npmmirror.com/@docusaurus/theme-common/-/theme-common-2.4.3.tgz" integrity sha512-7KaDJBXKBVGXw5WOVt84FtN8czGWhM0lbyWEZXGp8AFfL6sZQfRTluFp4QriR97qwzSyOfQb+nzcDZZU4tezUw== dependencies: "@docusaurus/mdx-loader" "2.4.3" @@ -1592,9 +1618,9 @@ use-sync-external-store "^1.2.0" utility-types "^3.10.0" -"@docusaurus/theme-search-algolia@2.4.3", "@docusaurus/theme-search-algolia@^2.4.3": +"@docusaurus/theme-search-algolia@^2.4.3", "@docusaurus/theme-search-algolia@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz#32d4cbefc3deba4112068fbdb0bde11ac51ece53" + resolved "https://registry.npmmirror.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz" integrity sha512-jziq4f6YVUB5hZOB85ELATwnxBz/RmSLD3ksGQOLDPKVzat4pmI8tddNWtriPpxR04BNT+ZfpPUMFkNFetSW1Q== dependencies: "@docsearch/react" "^3.1.1" @@ -1616,15 +1642,15 @@ "@docusaurus/theme-translations@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz#91ac73fc49b8c652b7a54e88b679af57d6ac6102" + resolved "https://registry.npmmirror.com/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz" integrity sha512-H4D+lbZbjbKNS/Zw1Lel64PioUAIT3cLYYJLUf3KkuO/oc9e0QCVhIYVtUI2SfBCF2NNdlyhBDQEEMygsCedIg== dependencies: fs-extra "^10.1.0" tslib "^2.4.0" -"@docusaurus/types@2.4.3": +"@docusaurus/types@*", "@docusaurus/types@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/types/-/types-2.4.3.tgz#4aead281ca09f721b3c0a9b926818450cfa3db31" + resolved "https://registry.npmmirror.com/@docusaurus/types/-/types-2.4.3.tgz" integrity sha512-W6zNLGQqfrp/EoPD0bhb9n7OobP+RHpmvVzpA+Z/IuU3Q63njJM24hmT0GYboovWcDtFmnIJC9wcyx4RVPQscw== dependencies: "@types/history" "^4.7.11" @@ -1638,14 +1664,14 @@ "@docusaurus/utils-common@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/utils-common/-/utils-common-2.4.3.tgz#30656c39ef1ce7e002af7ba39ea08330f58efcfb" + resolved "https://registry.npmmirror.com/@docusaurus/utils-common/-/utils-common-2.4.3.tgz" integrity sha512-/jascp4GbLQCPVmcGkPzEQjNaAk3ADVfMtudk49Ggb+131B1WDD6HqlSmDf8MxGdy7Dja2gc+StHf01kiWoTDQ== dependencies: tslib "^2.4.0" "@docusaurus/utils-validation@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/utils-validation/-/utils-validation-2.4.3.tgz#8122c394feef3e96c73f6433987837ec206a63fb" + resolved "https://registry.npmmirror.com/@docusaurus/utils-validation/-/utils-validation-2.4.3.tgz" integrity sha512-G2+Vt3WR5E/9drAobP+hhZQMaswRwDlp6qOMi7o7ZypB+VO7N//DZWhZEwhcRGepMDJGQEwtPv7UxtYwPL9PBw== dependencies: "@docusaurus/logger" "2.4.3" @@ -1656,7 +1682,7 @@ "@docusaurus/utils@2.4.3": version "2.4.3" - resolved "https://registry.npmmirror.com/@docusaurus/utils/-/utils-2.4.3.tgz#52b000d989380a2125831b84e3a7327bef471e89" + resolved "https://registry.npmmirror.com/@docusaurus/utils/-/utils-2.4.3.tgz" integrity sha512-fKcXsjrD86Smxv8Pt0TBFqYieZZCPh4cbf9oszUq/AMhZn3ujwpKaVYZACPX8mmjtYx0JOgNx52CREBfiGQB4A== dependencies: "@docusaurus/logger" "2.4.3" @@ -1690,7 +1716,7 @@ "@headlessui/react@^1.7.17": version "1.7.17" - resolved "https://registry.npmmirror.com/@headlessui/react/-/react-1.7.17.tgz#a0ec23af21b527c030967245fd99776aa7352bc6" + resolved "https://registry.npmmirror.com/@headlessui/react/-/react-1.7.17.tgz" integrity sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow== dependencies: client-only "^0.0.1" @@ -1741,16 +1767,16 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.15" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.18" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" @@ -1807,7 +1833,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -1915,7 +1941,7 @@ "@svgr/babel-plugin-transform-react-native-svg" "^6.5.1" "@svgr/babel-plugin-transform-svg-component" "^6.5.1" -"@svgr/core@^6.5.1": +"@svgr/core@*", "@svgr/core@^6.0.0", "@svgr/core@^6.5.1": version "6.5.1" resolved "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz" integrity sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw== @@ -2110,12 +2136,7 @@ dependencies: "@types/unist" "*" -"@types/mime@*": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== - -"@types/mime@^1": +"@types/mime@*", "@types/mime@^1": version "1.3.2" resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== @@ -2181,7 +2202,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*": +"@types/react@*", "@types/react@>= 16.8.0 < 19.0.0": version "18.2.7" resolved "https://registry.npmjs.org/@types/react/-/react-18.2.7.tgz" integrity sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw== @@ -2261,7 +2282,7 @@ dependencies: "@types/yargs-parser" "*" -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": +"@webassemblyjs/ast@^1.11.5", "@webassemblyjs/ast@1.11.6": version "1.11.6" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz" integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== @@ -2362,7 +2383,7 @@ "@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-parser" "1.11.6" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": +"@webassemblyjs/wasm-parser@^1.11.5", "@webassemblyjs/wasm-parser@1.11.6": version "1.11.6" resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz" integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== @@ -2410,7 +2431,7 @@ acorn-walk@^8.0.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1: +acorn@^8, acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -2447,7 +2468,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2457,7 +2478,17 @@ ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.9.0: +ajv@^8.0.0: + version "8.12.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^8.8.2, ajv@^8.9.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -2474,7 +2505,7 @@ algoliasearch-helper@^3.10.0: dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.0.0, algoliasearch@^4.13.1: +algoliasearch@^4.0.0, algoliasearch@^4.13.1, "algoliasearch@>= 3.1 < 6", "algoliasearch@>= 4.9.1 < 6": version "4.17.1" resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.17.1.tgz" integrity sha512-4GDQ1RhP2qUR3x8PevFRbEdqZqIARNViZYjgTJmA1T7wRNtFA3W4Aqc/RsODqa1J8IO/QDla5x4tWuUS8NV8wA== @@ -2542,7 +2573,7 @@ ansi-styles@^6.1.0: any-promise@^1.0.0: version "1.3.0" - resolved "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + resolved "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz" integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== anymatch@~3.1.2: @@ -2570,16 +2601,16 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - array-flatten@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" @@ -2595,21 +2626,9 @@ at-least-node@^1.0.0: resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@^10.4.12, autoprefixer@^10.4.7: - version "10.4.14" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== - dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -autoprefixer@^10.4.16: +autoprefixer@^10.4.12, autoprefixer@^10.4.16, autoprefixer@^10.4.7: version "10.4.16" - resolved "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8" + resolved "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.16.tgz" integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ== dependencies: browserslist "^4.21.10" @@ -2793,19 +2812,9 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: - version "4.21.7" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz" - integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA== - dependencies: - caniuse-lite "^1.0.30001489" - electron-to-chromium "^1.4.411" - node-releases "^2.0.12" - update-browserslist-db "^1.0.11" - -browserslist@^4.21.10: +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5, "browserslist@>= 4.21.0": version "4.22.1" - resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.22.1.tgz" integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== dependencies: caniuse-lite "^1.0.30001541" @@ -2862,7 +2871,7 @@ camel-case@^4.1.2: pascal-case "^3.1.2" tslib "^2.0.3" -camelcase-css@2.0.1, camelcase-css@^2.0.1: +camelcase-css@^2.0.1, camelcase-css@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== @@ -2882,14 +2891,9 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001489: - version "1.0.30001491" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001491.tgz" - integrity sha512-17EYIi4TLnPiTzVKMveIxU5ETlxbSO3B6iPvMbprqnKh4qJsQGk5Nh1Lp4jIMAE0XfrujsJuWZAM3oJdMHaKBA== - -caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: version "1.0.30001558" - resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz#d2c6e21fdbfe83817f70feab902421a19b7983ee" + resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz" integrity sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ== ccount@^1.0.0: @@ -2954,7 +2958,7 @@ cheerio@^1.0.0-rc.12: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: +chokidar@^3.4.2, chokidar@^3.5.3, "chokidar@>=3.0.0 <4.0.0": version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -3017,7 +3021,7 @@ cli-table3@^0.6.2: client-only@^0.0.1: version "0.0.1" - resolved "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + resolved "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== clone-deep@^4.0.1: @@ -3060,16 +3064,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + colord@^2.9.1: version "2.9.3" resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" @@ -3097,7 +3101,7 @@ commander@^2.20.0: commander@^4.0.0: version "4.1.1" - resolved "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + resolved "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== commander@^5.1.0: @@ -3435,20 +3439,27 @@ csstype@^3.0.2: resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== -debug@2.6.9, debug@^2.6.0: +debug@^2.6.0: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1: +debug@^4.1.0, debug@^4.1.1, debug@4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz" @@ -3505,16 +3516,16 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - depd@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + destroy@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" @@ -3550,7 +3561,7 @@ detect-port@^1.3.0: didyoumean@^1.2.2: version "1.2.2" - resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== dir-glob@^3.0.1: @@ -3562,7 +3573,7 @@ dir-glob@^3.0.1: dlv@^1.1.3: version "1.1.3" - resolved "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + resolved "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== dns-equal@^1.0.0: @@ -3663,19 +3674,19 @@ dot-prop@^5.2.0: dotenv@^16.3.1: version "16.3.1" - resolved "https://registry.npmmirror.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + resolved "https://registry.npmmirror.com/dotenv/-/dotenv-16.3.1.tgz" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== -duplexer3@^0.1.4: - version "0.1.5" - resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz" - integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== - duplexer@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +duplexer3@^0.1.4: + version "0.1.5" + resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz" + integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" @@ -3686,14 +3697,9 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.411: - version "1.4.413" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.413.tgz" - integrity sha512-Gd+/OAhRca06dkVxIQo/W7dr6Nmk9cx6lQdZ19GvFp51k5B/lUAokm6SJfNkdV8kFLsC3Z4sLTyEHWCnB1Efbw== - electron-to-chromium@^1.4.535: version "1.4.571" - resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.571.tgz#8aa71539eb82db98740c3ec861256cc34e0356fd" + resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.571.tgz" integrity sha512-Sc+VtKwKCDj3f/kLBjdyjMpNzoZsU6WuL/wFb6EH8USmHEcebxRXcRrVpOpayxd52tuey4RUDpUsw5OS5LhJqg== emoji-regex@^8.0.0: @@ -3915,20 +3921,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.3.0: +fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: version "3.3.1" - resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz" integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -3995,7 +3990,7 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" -file-loader@^6.2.0: +file-loader@*, file-loader@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== @@ -4097,14 +4092,9 @@ forwarded@0.2.0: resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== - fraction.js@^4.3.6: version "4.3.7" - resolved "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + resolved "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== fresh@0.5.2: @@ -4141,21 +4131,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -4207,7 +4187,14 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1, glob-parent@^6.0.2: +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -4219,27 +4206,27 @@ glob-to-regexp@^0.4.1: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmmirror.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== +glob@7.1.6: + version "7.1.6" + resolved "https://registry.npmmirror.com/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.1.1" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" @@ -4377,13 +4364,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hasown@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" - integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== - dependencies: - function-bind "^1.1.2" - hast-to-hyperscript@^9.0.0: version "9.0.1" resolved "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz" @@ -4555,6 +4535,16 @@ http-deceiver@^1.2.7: resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -4566,16 +4556,6 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" @@ -4676,7 +4656,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4686,16 +4666,16 @@ inherits@2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -ini@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - ini@^1.3.5, ini@~1.3.0: version "1.3.8" resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ini@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" @@ -4713,17 +4693,17 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - ipaddr.js@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== -is-alphabetical@1.0.4, is-alphabetical@^1.0.0: +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-alphabetical@^1.0.0, is-alphabetical@1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== @@ -4767,13 +4747,6 @@ is-core-module@^2.11.0: dependencies: has "^1.0.3" -is-core-module@^2.13.0: - version "2.13.1" - resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== - dependencies: - hasown "^2.0.0" - is-decimal@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz" @@ -4908,16 +4881,16 @@ is-yarn-global@^0.3.0: resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -4959,14 +4932,9 @@ jest-worker@^29.1.2: merge-stream "^2.0.0" supports-color "^8.0.0" -jiti@^1.18.2: - version "1.18.2" - resolved "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz" - integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg== - -jiti@^1.19.1: +jiti@^1.18.2, jiti@^1.19.1: version "1.21.0" - resolved "https://registry.npmmirror.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + resolved "https://registry.npmmirror.com/jiti/-/jiti-1.21.0.tgz" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== joi@^17.6.0: @@ -5157,7 +5125,7 @@ lodash.memoize@^4.1.2: resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== -lodash.uniq@4.5.0, lodash.uniq@^4.5.0: +lodash.uniq@^4.5.0, lodash.uniq@4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== @@ -5300,7 +5268,7 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +"mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -5310,14 +5278,40 @@ mime-db@~1.33.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz" integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== -mime-types@2.1.18: +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@^2.1.31: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@~2.1.17, mime-types@2.1.18: version "2.1.18" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz" integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== dependencies: mime-db "~1.33.0" -mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@~2.1.24: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -5351,20 +5345,13 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.1.2, minimatch@^3.1.1: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.4, minimatch@^3.0.5: - version "3.0.8" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz" - integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== - dependencies: - brace-expansion "^1.1.7" - minimist@^1.2.0, minimist@^1.2.5: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -5400,7 +5387,7 @@ multicast-dns@^7.2.5: mz@^2.7.0: version "2.7.0" - resolved "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + resolved "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz" integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== dependencies: any-promise "^1.0.0" @@ -5449,14 +5436,9 @@ node-forge@^1: resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-releases@^2.0.12: - version "2.0.12" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz" - integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== - node-releases@^2.0.13: version "2.0.13" - resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.13.tgz" integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== normalize-path@^3.0.0, normalize-path@~3.0.0: @@ -5505,7 +5487,7 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: object-hash@^3.0.0: version "3.0.0" - resolved "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + resolved "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== object-inspect@^1.9.0: @@ -5748,6 +5730,13 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" @@ -5758,13 +5747,6 @@ path-to-regexp@2.2.1: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz" integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -5782,12 +5764,12 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: pify@^2.3.0: version "2.3.0" - resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== pirates@^4.0.1: version "4.0.6" - resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== pkg-dir@^4.1.0: @@ -5859,7 +5841,7 @@ postcss-discard-unused@^5.1.0: postcss-import@^15.1.0: version "15.1.0" - resolved "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + resolved "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz" integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== dependencies: postcss-value-parser "^4.0.0" @@ -5868,14 +5850,14 @@ postcss-import@^15.1.0: postcss-js@^4.0.1: version "4.0.1" - resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz" integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== dependencies: camelcase-css "^2.0.1" postcss-load-config@^4.0.1: version "4.0.1" - resolved "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" + resolved "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz" integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA== dependencies: lilconfig "^2.0.5" @@ -5979,7 +5961,7 @@ postcss-modules-values@^4.0.0: postcss-nested@^6.0.1: version "6.0.1" - resolved "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" + resolved "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.0.1.tgz" integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== dependencies: postcss-selector-parser "^6.0.11" @@ -6117,18 +6099,9 @@ postcss-zindex@^5.1.0: resolved "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-5.1.0.tgz" integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== -postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.21: - version "8.4.24" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz" - integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.4.23, postcss@^8.4.31: +"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.0, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.16, postcss@^8.4.17, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.31, postcss@>=8.0.9: version "8.4.31" - resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz" integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" @@ -6158,7 +6131,7 @@ prism-react-renderer@^1.3.5: resolved "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz" integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg== -prismjs@^1.28.0, prismjs@^1.29.0: +prismjs@^1.18.0, prismjs@^1.28.0, prismjs@^1.29.0: version "1.29.0" resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz" integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== @@ -6263,16 +6236,21 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" - integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -range-parser@^1.2.1, range-parser@~1.2.1: +range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" + integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== + raw-body@2.5.1: version "2.5.1" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" @@ -6283,7 +6261,7 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -rc@1.2.8, rc@^1.2.8: +rc@^1.2.8, rc@1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -6333,7 +6311,7 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@^17.0.2: +react-dom@*, "react-dom@^16 || ^17 || ^18", "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.4 || ^17.0.0", "react-dom@^17.0.0 || ^16.3.0 || ^15.5.4", react-dom@^17.0.2, "react-dom@>= 16.8.0 < 19.0.0": version "17.0.2" resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -6365,7 +6343,7 @@ react-helmet-async@*, react-helmet-async@^1.3.0: react-helmet@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" + resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz" integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== dependencies: object-assign "^4.1.1" @@ -6400,9 +6378,9 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" -"react-loadable@npm:@docusaurus/react-loadable@5.5.2": +react-loadable@*, "react-loadable@npm:@docusaurus/react-loadable@5.5.2": version "5.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" + resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz" integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== dependencies: "@types/react" "*" @@ -6428,7 +6406,7 @@ react-router-dom@^5.3.3: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.3.4, react-router@^5.3.3: +react-router@^5.3.3, react-router@>=5, react-router@5.3.4: version "5.3.4" resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== @@ -6445,7 +6423,7 @@ react-router@5.3.4, react-router@^5.3.3: react-side-effect@^2.1.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" + resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz" integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== react-textarea-autosize@^8.3.2: @@ -6457,7 +6435,7 @@ react-textarea-autosize@^8.3.2: use-composed-ref "^1.3.0" use-latest "^1.2.1" -react@^17.0.2: +react@*, "react@^15.0.2 || ^16.0.0 || ^17.0.0", "react@^16 || ^17 || ^18", "react@^16.13.1 || ^17.0.0", "react@^16.3.0 || ^17.0.0 || ^18.0.0", "react@^16.6.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.4 || ^17.0.0", "react@^17.0.0 || ^16.3.0 || ^15.5.4", react@^17.0.2, "react@>= 16.8.0 < 19.0.0", react@>=0.14.9, react@>=15, react@>=16.3.0, react@17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== @@ -6467,7 +6445,7 @@ react@^17.0.2: read-cache@^1.0.0: version "1.0.0" - resolved "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + resolved "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz" integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== dependencies: pify "^2.3.0" @@ -6680,7 +6658,7 @@ resolve-pathname@^3.0.0: resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== -resolve@^1.1.6, resolve@^1.14.2, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.2, resolve@^1.3.2: version "1.22.2" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz" integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== @@ -6689,15 +6667,6 @@ resolve@^1.1.6, resolve@^1.14.2, resolve@^1.3.2: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.1.7, resolve@^1.22.2: - version "1.22.8" - resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - responselike@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" @@ -6751,15 +6720,20 @@ rxjs@^7.5.4: dependencies: tslib "^2.1.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" @@ -6777,7 +6751,7 @@ sass-loader@^10.1.1: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.62.1: +sass@^1.3.0, sass@^1.30.0, sass@^1.62.1: version "1.62.1" resolved "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz" integrity sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A== @@ -6799,15 +6773,6 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - schema-utils@^2.6.5: version "2.7.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" @@ -6817,7 +6782,25 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.1.2: +schema-utils@^3.0.0: + version "3.1.2" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz" + integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^3.1.1: + version "3.1.2" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz" + integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz" integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== @@ -6836,6 +6819,15 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz" @@ -6868,7 +6860,27 @@ semver@^5.4.1: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^6.1.1: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^6.1.2: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^6.2.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -7058,7 +7070,7 @@ sort-css-media-queries@2.1.0: resolved "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz" integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA== -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: +source-map-js@^1.0.2, "source-map-js@>=0.6.2 <2.0.0": version "1.0.2" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -7124,22 +7136,45 @@ state-toggle@^1.0.0: resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz" integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - "statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + std-env@^3.0.1: version "3.3.3" resolved "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz" integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg== -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.2: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7157,20 +7192,6 @@ string-width@^5.0.1: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" @@ -7214,7 +7235,7 @@ strip-json-comments@~2.0.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== -style-to-object@0.3.0, style-to-object@^0.3.0: +style-to-object@^0.3.0, style-to-object@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz" integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== @@ -7231,7 +7252,7 @@ stylehacks@^5.1.1: sucrase@^3.32.0: version "3.34.0" - resolved "https://registry.npmmirror.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" + resolved "https://registry.npmmirror.com/sucrase/-/sucrase-3.34.0.tgz" integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== dependencies: "@jridgewell/gen-mapping" "^0.3.2" @@ -7288,7 +7309,7 @@ svgo@^2.7.0, svgo@^2.8.0: tailwindcss@^3.3.3: version "3.3.5" - resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.5.tgz#22a59e2fbe0ecb6660809d9cc5f3976b077be3b8" + resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.5.tgz" integrity sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA== dependencies: "@alloc/quick-lru" "^5.2.0" @@ -7352,14 +7373,14 @@ text-table@^0.2.0: thenify-all@^1.0.0: version "1.6.0" - resolved "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + resolved "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz" integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== dependencies: thenify ">= 3.1.0 < 4" "thenify@>= 3.1.0 < 4": version "3.3.1" - resolved "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + resolved "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz" integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== dependencies: any-promise "^1.0.0" @@ -7428,7 +7449,7 @@ trough@^1.0.0: ts-interface-checker@^0.1.9: version "0.1.13" - resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: @@ -7461,7 +7482,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^5.0.4: +typescript@^5.0.4, "typescript@>= 2.7": version "5.0.4" resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz" integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== @@ -7502,10 +7523,10 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unified@9.2.0: - version "9.2.0" - resolved "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz" - integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg== +unified@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz" + integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== dependencies: bail "^1.0.0" extend "^3.0.0" @@ -7514,10 +7535,10 @@ unified@9.2.0: trough "^1.0.0" vfile "^4.0.0" -unified@^9.2.2: - version "9.2.2" - resolved "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz" - integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== +unified@9.2.0: + version "9.2.0" + resolved "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz" + integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg== dependencies: bail "^1.0.0" extend "^3.0.0" @@ -7533,7 +7554,7 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -unist-builder@2.0.3, unist-builder@^2.0.0: +unist-builder@^2.0.0, unist-builder@2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz" integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== @@ -7582,7 +7603,7 @@ unist-util-visit-parents@^3.0.0: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" -unist-util-visit@2.0.3, unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: +unist-util-visit@^2.0.0, unist-util-visit@^2.0.3, unist-util-visit@2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz" integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== @@ -7596,22 +7617,14 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - update-browserslist-db@^1.0.13: version "1.0.13" - resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz" integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== dependencies: escalade "^3.1.1" @@ -7689,7 +7702,7 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: utila@~0.4: version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz" integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== utility-types@^3.10.0: @@ -7852,7 +7865,7 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.73.0: +"webpack@^4.0.0 || ^5.0.0", "webpack@^4.36.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.73.0, "webpack@>= 4", webpack@>=2, "webpack@>=4.41.1 || 5.x", "webpack@3 || 4 || 5": version "5.84.1" resolved "https://registry.npmjs.org/webpack/-/webpack-5.84.1.tgz" integrity sha512-ZP4qaZ7vVn/K8WN/p990SGATmrL1qg4heP/MrVneczYtpDGJWlrgZv55vxaV2ul885Kz+25MP2kSXkPe3LZfmg== @@ -7892,7 +7905,7 @@ webpackbar@^5.0.2: pretty-time "^1.1.0" std-env "^3.0.1" -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: +websocket-driver@^0.7.4, websocket-driver@>=0.5.1: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== @@ -8031,7 +8044,7 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: yaml@^2.1.1: version "2.3.3" - resolved "https://registry.npmmirror.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" + resolved "https://registry.npmmirror.com/yaml/-/yaml-2.3.3.tgz" integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== yocto-queue@^0.1.0: diff --git a/extensions/ide/vscode/devbox/CHANGELOG.md b/extensions/ide/vscode/devbox/CHANGELOG.md index bec8e4d2259a..fd285c0a7f0e 100644 --- a/extensions/ide/vscode/devbox/CHANGELOG.md +++ b/extensions/ide/vscode/devbox/CHANGELOG.md @@ -6,6 +6,18 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +## [1.3.4] - 2025-02-18 + +### Fixed + +- Windsurf can not open. + +## [1.3.1] - 2025-01-21 + +### Added + +- Support Windsurf and Trae. + ## [1.3.0] - 2024-12-24 ### Fixed diff --git a/extensions/ide/vscode/devbox/package.json b/extensions/ide/vscode/devbox/package.json index 5329d79ee141..70b7c506fffd 100644 --- a/extensions/ide/vscode/devbox/package.json +++ b/extensions/ide/vscode/devbox/package.json @@ -2,7 +2,7 @@ "name": "devbox-aio", "displayName": "%displayName%", "description": "%description%", - "version": "1.3.0", + "version": "1.3.4", "keywords": [ "devbox", "remote development", diff --git a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts index 82000f5e4e5b..31db2b95d452 100644 --- a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts +++ b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts @@ -256,13 +256,19 @@ export class RemoteSSHConnector extends Disposable { vscode.env.uriScheme === 'vscode' || vscode.env.uriScheme === 'vscode-insiders' || vscode.env.uriScheme === 'cursor' - if (!isOfficialVscode) { + const isTrae = vscode.env.uriScheme === 'trae' + + // windsurf has remote-ssh inside already + if (!isOfficialVscode && !isTrae) { return true } - const msVscodeRemoteExt = vscode.extensions.getExtension( - 'ms-vscode-remote.remote-ssh' - ) + const remoteSSHId = isOfficialVscode + ? 'ms-vscode-remote.remote-ssh' + : 'labring.open-remote-ssh-for-trae' + + const msVscodeRemoteExt = vscode.extensions.getExtension(remoteSSHId) + if (msVscodeRemoteExt) { return true } @@ -281,16 +287,13 @@ export class RemoteSSHConnector extends Disposable { return false } - await vscode.commands.executeCommand( - 'extension.open', - 'ms-vscode-remote.remote-ssh' - ) + await vscode.commands.executeCommand('extension.open', remoteSSHId) await vscode.commands.executeCommand( 'workbench.extensions.installExtension', - 'ms-vscode-remote.remote-ssh' + remoteSSHId ) - Logger.info('"ms-vscode-remote.remote-ssh" extension is installed') + Logger.info(`"${remoteSSHId}" extension is installed`) return true } diff --git a/extensions/ide/vscode/devbox/src/providers/DBViewProvider.ts b/extensions/ide/vscode/devbox/src/providers/DBViewProvider.ts index d97f38fa8306..b6fa9b077a47 100644 --- a/extensions/ide/vscode/devbox/src/providers/DBViewProvider.ts +++ b/extensions/ide/vscode/devbox/src/providers/DBViewProvider.ts @@ -123,7 +123,6 @@ export class DBViewProvider await this.refreshDatabases() } private async openWebTerminal(dbInfo: Database) { - console.log('dbInfo', dbInfo) const commandMap = { postgresql: `psql '${dbInfo.connection}'`, mongodb: `mongosh '${dbInfo.connection}'`, diff --git a/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts index 74d22e522925..8dbb1fb02ba7 100644 --- a/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts +++ b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts @@ -237,30 +237,33 @@ class ProjectTreeDataProvider } try { + const appName = vscode.env.appName + // 1. remove global state GlobalStateManager.remove(deletedHost) - // 2. remove remote-ssh config - const existingSSHHostPlatforms = vscode.workspace - .getConfiguration('remote.SSH') - .get<{ [host: string]: string }>('remotePlatform', {}) - const newSSHHostPlatforms = Object.keys(existingSSHHostPlatforms).reduce( - (acc: { [host: string]: string }, host: string) => { + // 2. remove remote-ssh config if app is not windsurf or Trae + if (appName !== 'Windsurf' && appName !== 'Trae') { + const existingSSHHostPlatforms = vscode.workspace + .getConfiguration('remote.SSH') + .get<{ [host: string]: string }>('remotePlatform', {}) + const newSSHHostPlatforms = Object.keys( + existingSSHHostPlatforms + ).reduce((acc: { [host: string]: string }, host: string) => { if (host.startsWith(deletedHost)) { return acc } acc[host] = existingSSHHostPlatforms[host] return acc - }, - {} - ) - await vscode.workspace - .getConfiguration('remote.SSH') - .update( - 'remotePlatform', - newSSHHostPlatforms, - vscode.ConfigurationTarget.Global - ) + }, {}) + await vscode.workspace + .getConfiguration('remote.SSH') + .update( + 'remotePlatform', + newSSHHostPlatforms, + vscode.ConfigurationTarget.Global + ) + } // 3. remove ssh config const content = await fs.promises.readFile( diff --git a/extensions/ide/vscode/devbox/src/utils/handleUri.ts b/extensions/ide/vscode/devbox/src/utils/handleUri.ts index 4ed00928adef..899a6d3ca32f 100644 --- a/extensions/ide/vscode/devbox/src/utils/handleUri.ts +++ b/extensions/ide/vscode/devbox/src/utils/handleUri.ts @@ -20,7 +20,8 @@ export class UriHandler { uri.scheme !== 'vscode' && uri.scheme !== 'cursor' && uri.scheme !== 'vscode-insiders' && - uri.scheme !== 'windsurf' + uri.scheme !== 'windsurf' && + uri.scheme !== 'trae' ) { return } diff --git a/extensions/ide/vscode/devbox/src/utils/remoteSSHConfig.ts b/extensions/ide/vscode/devbox/src/utils/remoteSSHConfig.ts index 8c64e4af8f8a..b40666306e55 100644 --- a/extensions/ide/vscode/devbox/src/utils/remoteSSHConfig.ts +++ b/extensions/ide/vscode/devbox/src/utils/remoteSSHConfig.ts @@ -1,35 +1,38 @@ -import * as vscode from 'vscode' -import { Logger } from '../common/logger' +import * as vscode from "vscode"; +import { Logger } from "../common/logger"; // update Remote-SSH config export const modifiedRemoteSSHConfig = async (sshHostLabel: string) => { - Logger.info(`Modifying Remote-SSH config for ${sshHostLabel}`) + Logger.info(`Modifying Remote-SSH config for ${sshHostLabel}`); const existingSSHHostPlatforms = vscode.workspace - .getConfiguration('remote.SSH') - .get<{ [host: string]: string }>('remotePlatform', {}) + .getConfiguration("remote.SSH") + .get<{ [host: string]: string }>("remotePlatform", {}); // delete repeated remotePlatform by sshDomain_namespace_devboxName const newSSHHostPlatforms = Object.keys(existingSSHHostPlatforms).reduce( (acc: { [host: string]: string }, host: string) => { if (host.startsWith(sshHostLabel)) { - return acc + return acc; } - acc[host] = existingSSHHostPlatforms[host] - return acc + acc[host] = existingSSHHostPlatforms[host]; + return acc; }, {} - ) + ); // add new ssh host label - newSSHHostPlatforms[sshHostLabel] = 'linux' + newSSHHostPlatforms[sshHostLabel] = "linux"; - await vscode.workspace - .getConfiguration('remote.SSH') - .update( - 'remotePlatform', - newSSHHostPlatforms, - vscode.ConfigurationTarget.Global - ) + const appName = vscode.env.appName; + if (appName !== "Windsurf" && appName !== "Trae") { + await vscode.workspace + .getConfiguration("remote.SSH") + .update( + "remotePlatform", + newSSHHostPlatforms, + vscode.ConfigurationTarget.Global + ); + } // await vscode.workspace // .getConfiguration('remote.SSH') @@ -41,5 +44,5 @@ export const modifiedRemoteSSHConfig = async (sshHostLabel: string) => { // .getConfiguration('remote.SSH') // .update('useLocalServer', true, vscode.ConfigurationTarget.Global) - Logger.info(`Modified Remote-SSH config for ${sshHostLabel}`) -} + Logger.info(`Modified Remote-SSH config for ${sshHostLabel}`); +}; diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 3d4809f1f906..b62dc3792d1f 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -45,7 +45,7 @@ FROM deps AS builder # Next.js collects completely anonymous telemetry data about general usage. # Learn more here: https://nextjs.org/telemetry # Uncomment the following line in case you want to disable telemetry during the build. -ENV NEXT_TELEMETRY_DISABLED 1 +ENV NEXT_TELEMETRY_DISABLED=1 # COPY --from=deps /app/packages ./packages @@ -62,9 +62,9 @@ fi # Production image, copy all the files and run next FROM base AS runner -ENV NODE_ENV production +ENV NODE_ENV=production # Uncomment the following line in case you want to disable telemetry during runtime. -ENV NEXT_TELEMETRY_DISABLED 1 +ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs @@ -96,7 +96,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/$path/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/$path/.next/static ./$path/.next/static EXPOSE 3000 -ENV PORT 3000 +ENV PORT=3000 ENV launchpath=./${path}/server.js diff --git a/frontend/README.md b/frontend/README.md index ea1c24f7d977..29edd7574417 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -68,7 +68,7 @@ make push-images DOCKER_USERNAME= IMAGE_TAG= Refer to other apps to add some configuration. -1. .github/workflows/frontend.yml +1. .github/workflows/frontends.yml 2. deploy/cloud/init.sh 3. deploy/cloud/scripts/init.sh 4. frontend/providers/app/deploy/manifests/appcr.yaml.tmpl diff --git a/frontend/desktop/.gitignore b/frontend/desktop/.gitignore index 3d4d46cf7ef3..329a68a1d110 100644 --- a/frontend/desktop/.gitignore +++ b/frontend/desktop/.gitignore @@ -42,5 +42,6 @@ yalc.lock config.yaml .env +.env.local data/config.local.yaml #/prisma/region/generated/ diff --git a/frontend/desktop/package.json b/frontend/desktop/package.json index 7a7be9f5adad..5a3379fb9d6d 100644 --- a/frontend/desktop/package.json +++ b/frontend/desktop/package.json @@ -34,6 +34,7 @@ "@sealos/driver": "workspace:^", "@sealos/ui": "workspace:^", "@tanstack/react-query": "^4.35.3", + "@umami/node": "^0.4.0", "axios": "^1.5.1", "clsx": "^1.2.1", "cors": "^2.8.5", @@ -42,7 +43,6 @@ "decimal.js": "^10.4.3", "eslint": "8.38.0", "eslint-config-next": "13.3.0", - "formidable": "^3.5.1", "framer-motion": "^10.16.4", "i18next": "^23.11.5", "immer": "^10.0.2", @@ -81,7 +81,6 @@ "devDependencies": { "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", - "@types/formidable": "^3.4.5", "@types/jest": "^29.5.10", "@types/js-cookie": "^3.0.4", "@types/js-yaml": "^4.0.6", @@ -93,6 +92,7 @@ "@types/nprogress": "^0.2.1", "@types/react": "18.2.37", "@types/react-dom": "18.0.11", + "@types/umami-browser": "^2.3.2", "@types/uuid": "^9.0.4", "dotenv-cli": "^7.3.0", "jest": "^29.7.0", diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index 49352373b0ac..3dec81410663 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -3,6 +3,9 @@ "accept": "Accept", "accept_invitation": "Accept Invitation", "access": "Access", + "account_bank_required": "Please enter the correct bank name of the enterprise", + "account_number": "Bank Account", + "account_number_required": "Please enter the correct recipient bank account of the enterprise", "account_settings": "Account Settings", "added": "Added", "agree_policy": "I have read and agree to the", @@ -19,6 +22,7 @@ "attachment": "appendix", "avatar": "Avatar", "balance": "Balance", + "bank_name": "Bank", "bind": "Link", "bind_success": "Binding successful", "bonus": "Bonus", @@ -39,6 +43,9 @@ "confirm": "Confirm", "confirm_again": "confirm again", "confirmnewpassword": "Confirm New Password", + "contact_info": "Phone Number", + "contact_info_must_be_numeric": "It must be a numeric string", + "contact_info_required": "Please enter the correct contact information", "core": "Core", "cost_center": "Cost Center", "create_team": "Create Workspace", @@ -67,9 +74,22 @@ "emailchangesuccess": "Email modified successfully", "enter": "Enter", "enter_confirm": "Please enter {{value}} to confirm", + "enterpriseKey": "Enterprise Name (Bank Account Name)", + "enterpriseKeyPlaceholder": "Enter the name of your enterprise", + "enterprise_auth_tips": "We will deposit a small amount into your enterprise bank account.\nConfirm the amount to complete verification. ", + "enterprise_key": "Unified Social Credit Code", + "enterprise_key_required": "Please enter the correct Unified Social Credit Code (USCC) of the enterprise", + "enterprise_keyname": "Enterprise Name (Bank Account Name) ", + "enterprise_keyname_placeholder": "Enter the name of your enterprise", "enterprise_name": "Company name", - "enterprise_name_required": "Please enter the correct company name", - "enterprise_verification": "Enterprise real name", + "enterprise_name_required": "Please enter the correct enterprise name", + "enterprise_realname_cancel_failed": "Cancellation failed.", + "enterprise_realname_cancel_success": "Cancellation successful.", + "enterprise_realname_payment_failed": "Verify that the payment amount failed to be transferred.", + "enterprise_realname_payment_success": "Verify that the amount has been transferred to your company's bank account.", + "enterprise_realname_verify_failed": "Enterprise real-name verification failed.", + "enterprise_realname_verify_success": "Enterprise real-name verification successful.", + "enterprise_verification": "Enterprise", "expected_to_use_next_month": "Usage for the next 30 days", "expected_used": "Estimated Runaway", "face_recognition_failed": "Personal real name failed", @@ -83,6 +103,7 @@ "generate_invitation_link": "Generate invitation link", "get_code": "verification", "get_code_failed": "Get code failed", + "get_verification_amount": "Request", "gift_amount": "Reward {{amount}} balance.", "github": "Github", "google": "Google", @@ -109,6 +130,7 @@ "invalid_phone_number": "Invalid phone number", "invalid_user_id": "Invalid User ID", "invalid_username_or_password": "Invalid username or password", + "invalid_verification_amount": "The verification amount format is invalid", "invalid_verification_code": "Invalid verification code", "invitation_reminder": "Invitation reminder", "invite_member": "Invite Member", @@ -118,8 +140,10 @@ "laf_on_sealos": "Laf on Sealos", "language": "Language", "launch_various_third-party_applications_with_one_click": "Launch various third-party applications with one click", + "legal_person": "Legal Representative", "license_buy": "License Buy", "link": "link", + "link_to_workorder": "Submit a Ticket", "loading": "Loading", "log_in": "Log In", "log_out": "Log Out", @@ -149,7 +173,7 @@ "next_time": "Next", "nickname": "Nickname", "no_apps_found": "No Apps Found", - "no_realname_auth": "NO REALNAME AUTH", + "no_realname_auth": "Identity Not Verified", "notification": "Notification", "noworkspacecreated": "You haven't created a workspace yet", "official_account_login": "Official account login", @@ -168,17 +192,18 @@ "payment_result": "Payment Result", "payment_status": "Payment Status", "payment_successful": "Payment Successful", - "personal_verification": "Personal real name", + "personal_verification": "Individual", "phone": "Phone", "phone_number_tips": "Phone Number", "phonechangesuccess": "Mobile phone number modified successfully", "please_enter": "Please enter", + "please_enter_account_number": "Enter the bank account number", + "please_enter_bank_name": "Enter a bank name", + "please_enter_contact_info": "Enter the phone number", + "please_enter_enterprise_key": "Enter the Unified Social Credit Code", + "please_enter_legal_person": "Enter the name of the legal representative", "please_enter_username": "Please enter your username", - "please_enter_your_enterprise_name": "Please enter your business name", - "please_fill_all_fields": "File cannot be empty", - "please_read_and_agree_to_the_agreement": "Please read and agree to the agreement", - "please_upload_the_business_license": "Please upload a photo of your business license", - "please_upload_the_supporting_materials": "Please sign and stamp the downloaded attachment and upload a photo", + "please_enter_verification_amount": "Enter the verification amount (in cents).", "privacy_policy": "Privacy Policy", "private_team_id_of_user": "User's ID", "purchase_history": "Purchase History", @@ -190,12 +215,11 @@ "read_all": "Read All", "read_and_agree": "Please read and agree to the agreement below", "realNameVerification": "Real Name Verification", - "realName_verification": "Real Name Verification", + "realName_verification": "Identity Verification", "realname_auth_now": "Click to verify your name", "realname_auth_reminder": "Real-name authentication reminder", "realname_auth_reminder_desc": "Real-name verification is required for regions in China. Without real-name verification, top-up will be restricted. Successful real-name verification will be rewarded with a {{reward}} Sealos balance.", - "realname_auth_tips_a": "1. Please ensure that the name, ID number and mobile phone number filled in are consistent.", - "realname_auth_tips_b": "2. The number segments provided by some virtual operators may not pass verification. Please apply for a work order and pass manual verification.", + "realname_auth_reminder_desc_no_reward": "Real-name verification is required for regions in China. Without real-name verification, top-up will be restricted.", "realname_info": "RealName", "receive_tips": "{{managerName}} invite you join in {{teamName}} as {{role}}", "recharge_amount": "Recharge Amount", @@ -208,6 +232,7 @@ "remain_other_region_resource_tips": "There are still associated resources that have not been deleted in your account. To help you successfully complete the account cancellation process, please clean up all region resources to ensure that nothing is missing.", "remain_template_tips": "There are still undeleted template resources in your account. To help you smoothly complete the account cancellation process, please manually delete all template resources to avoid data loss", "remain_workspace_tips": "There are still undeleted associated resources in your account. To help you successfully complete the account cancellation process, please clean up or transfer your workspace to avoid data loss.", + "remaining_attempts": "{{count}} times left", "remaining_time": "Remaining Time: ", "remove": "Remove", "remove_member_tips": "Determine that you want to remove the member?", @@ -229,22 +254,23 @@ "status": "Status", "storage": "Storage", "submit_error": "Submit Error", - "supporting_materials": "Proof material", + "submit_verification": "Submit ", "switching_disc": "Switching Disc", "team": "Workspace", "terminal": "Terminal", "the_invited_user_must_be_others": "The invited user must be others", "toggle_app_bar": "Toggle App Bar", "total_amount": "Total Amount", + "transAmt_not_match": "The verification amount does not match. Please note that the verification amount is in cents. For example, if the received amount is 0.23 yuan, enter 23.", "unbind": "Unbind", "unbind_success": "Unbinding successfully", "unbound": "Not Linked", "under_active_development": "Under active development 🚧", "unread": "Unread", - "upload_success": "Upload successful", "used_last_month": "Usage for the last 30 days", "used_resources": "Resources Used", "user_name": "User Name", + "user_name_required": "Please enter the correct name of the enterprise's legal representative", "username": "Username", "username_tips": "Username must be 3-16 characters, including letters, numbers", "usertask": { @@ -255,6 +281,10 @@ "task_launchpad_desc": "Create a container cluster with one click, automatically deploy container applications, and provide intranet/extranet access addresses", "task_launchpad_title": "Deploy App" }, + "verification_amount": "Verification Amount", + "verification_amount_required": "The verification amount cannot be empty", + "verification_amount_tips": "Shanghai UnionPay will deposit a random amount into your bank account for\nverification, typically arriving in real-time. If not received in 3 business day, please ", + "verification_amount_tips2": "Please confirm the deposited verification amount with your finance team. It usually arrives in real-time.", "verification_code_login": "with Phone", "verify_code_tips": "6-digit Verification Code", "verify_password": "Verify password", @@ -272,4 +302,4 @@ "you_can_view_fees_through_the_fee_center": "You can view fees through the fee center", "you_have_not_purchased_the_license": "You have not purchased the License", "yuan": "Yuan" -} \ No newline at end of file +} diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index a21880bd6044..1e56f9d94f35 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -3,6 +3,9 @@ "accept": "接受", "accept_invitation": "接受邀请", "access": "权限", + "account_bank_required": "请输入正确的企业开户行", + "account_number": "银行账号", + "account_number_required": "请输入正确的企业开户行收款银行账号", "account_settings": "账户设置", "added": "已加入", "agree_policy": "我已阅读并同意", @@ -18,6 +21,7 @@ "attachment": "附件", "avatar": "头像", "balance": "余额", + "bank_name": "开户银行", "bind": "绑定", "bind_success": "绑定成功", "bonus": "赠", @@ -38,6 +42,9 @@ "confirm": "确认", "confirm_again": "再次确认", "confirmnewpassword": "确认新密码", + "contact_info": "联系方式 (手机号码)", + "contact_info_must_be_numeric": "必须是数字字符串", + "contact_info_required": "请输入正确的联系方式", "core": "核", "create_team": "创建工作空间", "created_time": "创建时间", @@ -64,8 +71,19 @@ "emailchangesuccess": "电子邮箱修改成功", "enter": "输入", "enter_confirm": "请输入 {{value}} 确认", + "enterprise_auth_tips": "我们会向你提交的对公账户进行小额打款, 请输入收到的金额完成验证。", + "enterprise_key": "统一社会信用代码", + "enterprise_key_required": "请输入正确的企业统一社会信用代码", + "enterprise_keyname": "企业名称 (需与银行开户名一致)", + "enterprise_keyname_placeholder": "请输入企业名称 (需与银行开户名一致)", "enterprise_name": "企业名称", "enterprise_name_required": "请输入正确的企业名字", + "enterprise_realname_cancel_failed": "取消失败", + "enterprise_realname_cancel_success": "取消成功", + "enterprise_realname_payment_failed": "验证金额打款失败", + "enterprise_realname_payment_success": "验证金额已经打款到您的企业开户银行", + "enterprise_realname_verify_failed": "企业实名失败", + "enterprise_realname_verify_success": "企业实名成功", "enterprise_verification": "企业实名", "expected_to_use_next_month": "未来30天预计使用", "expected_used": "预计还能使用", @@ -80,6 +98,7 @@ "generate_invitation_link": "生成邀请链接", "get_code": "获取验证码", "get_code_failed": "获取验证码失败", + "get_verification_amount": "获取验证金额", "gift_amount": "赠送 {{amount}} 余额.", "github": "Github", "google": "Google", @@ -106,6 +125,7 @@ "invalid_phone_number": "无效的手机号", "invalid_user_id": "用户的 ID 不合法", "invalid_username_or_password": "用户名或密码错误", + "invalid_verification_amount": "验证金额格式不合法", "invalid_verification_code": "无效的验证码", "invitation_reminder": "受邀提醒", "invite_member": "邀请成员", @@ -114,8 +134,10 @@ "jump_over": "跳过", "language": "语言", "launch_various_third-party_applications_with_one_click": "一键启动各种第三方应用", + "legal_person": "法定代表人", "license_buy": "License 购买", "link": "链接", + "link_to_workorder": "提交工单", "loading": "加载中", "log_in": "登录", "log_out": "登出", @@ -169,12 +191,13 @@ "phone_number_tips": "手机号", "phonechangesuccess": "手机号修改成功", "please_enter": "请输入", + "please_enter_account_number": "请输入银行账号", + "please_enter_bank_name": "请输入开户行名", + "please_enter_contact_info": "请输入联系方式", + "please_enter_enterprise_key": "请输入统一社会信用代码", + "please_enter_legal_person": "请输入法人姓名", "please_enter_username": "请输入您的用户名", - "please_enter_your_enterprise_name": "请输入您的企业名称", - "please_fill_all_fields": "文件不能为空", - "please_read_and_agree_to_the_agreement": "请阅读并同意协议", - "please_upload_the_business_license": "请上传企业的营业执照照片", - "please_upload_the_supporting_materials": "请在下载的附件上签名并盖好公章,上传照片", + "please_enter_verification_amount": "请输入验证金额(单位分)", "privacy_policy": "隐私政策", "private_team_id_of_user": "用户ID", "purchase_history": "购买记录", @@ -190,8 +213,7 @@ "realname_auth_now": "点击进行实名", "realname_auth_reminder": "实名认证提醒", "realname_auth_reminder_desc": "国内可用区需要实名认证,未实名认证将会被限制充值,实名认证成功奖励 Sealos 余额 {{reward}} 元。", - "realname_auth_tips_a": "1、请确保所填写的姓名、身份证号码和手机号码信息一致。", - "realname_auth_tips_b": "2、部分虚拟运营商提供的号段可能无法验证通过,请申请工单通过人工协助认证。", + "realname_auth_reminder_desc_no_reward": "国内可用区需要实名认证,未实名认证将会被限制充值。", "realname_info": "实名信息", "receive_tips": "{{managerName}} 邀请你到 {{teamName}} 成为 {{role}}", "recharge_amount": "充值金额", @@ -204,6 +226,7 @@ "remain_other_region_resource_tips": "您好,您的账户中仍有未删除的关联资源,为了帮助您顺利完成账户注销流程,请您清理所有可用区资源,确保无遗漏。", "remain_template_tips": "您的账户中仍有未删除的模板资源,为了帮助您顺利完成账户注销流程,请您手动删除所有模板资源,以避免数据丢失", "remain_workspace_tips": "您的账户中仍有未删除的关联资源,为了帮助您顺利完成账户注销流程,请您清理或转移您的工作空间,以避免数据丢失", + "remaining_attempts": "剩余 {{count}} 次", "remaining_time": "剩余激活时间: ", "remove": "移除", "remove_member_tips": "确认要移除该成员?", @@ -224,20 +247,21 @@ "status": "状态", "storage": "存储", "submit_error": "提交错误", - "supporting_materials": "证明材料", + "submit_verification": "提交认证 ", "switching_disc": "切换圆盘", "team": "工作空间", "the_invited_user_must_be_others": "只能邀请其他人", "toggle_app_bar": "切换应用栏", + "transAmt_not_match": "验证金额不匹配,注意验证金额单位是分,例如收到的打款金额为 0.23 元 则输入23。", "unbind": "解绑", "unbind_success": "解绑成功", "unbound": "未绑定", "under_active_development": "正在积极开发中 🚧", "unread": "未读", - "upload_success": "上传成功", "used_last_month": "过去30天已使用", "used_resources": "已用资源", "user_name": "用户名", + "user_name_required": "请输入正确的企业法人姓名", "username": "用户名", "username_tips": "用户名为3-16位的英文或数字的字符", "usertask": { @@ -248,6 +272,10 @@ "task_launchpad_desc": "一键创建容器集群,自动化 部署容器应用,并提供内 网/外网访问地址", "task_launchpad_title": "部署应用" }, + "verification_amount": "验证金额", + "verification_amount_required": "验证金额不能为空", + "verification_amount_tips": "验证金额将由上海银联打入你提交的对公账号, 金额随机, 一般实时到账。若三个工作日内未收到, 请", + "verification_amount_tips2": "请联系公司财务确认企业银行账户收到的验证金额, 一般实时到账, 长期有效。", "verification_code_login": "手机号登录", "verify_code_tips": "6位验证码", "verify_password": "确认密码", diff --git a/frontend/desktop/src/api/auth.ts b/frontend/desktop/src/api/auth.ts index 33ee7e991d78..658468914555 100644 --- a/frontend/desktop/src/api/auth.ts +++ b/frontend/desktop/src/api/auth.ts @@ -6,6 +6,7 @@ import { ApiResp, Region } from '@/types'; import { BIND_STATUS } from '@/types/response/bind'; import { RESOURCE_STATUS } from '@/types/response/checkResource'; import { DELETE_USER_STATUS } from '@/types/response/deleteUser'; +import { EnterpriseAuthInfo, PAYMENTSTATUS } from '@/types/response/enterpriseRealName'; import { USER_MERGE_STATUS } from '@/types/response/merge'; import { UNBIND_STATUS } from '@/types/response/unbind'; import { SemData } from '@/types/sem'; @@ -58,7 +59,6 @@ export const _UserInfo = (request: AxiosInstance) => () => ApiResp<{ info: { realName?: string; - enterpriseVerificationStatus?: string; enterpriseRealName?: string; userRestrictedLevel?: number; uid: string; @@ -172,27 +172,52 @@ export const _getFaceAuthStatusRequest = (request: AxiosInstance) => (data: { bi data ); -export const _enterpriseRealNameAuthRequest = (request: AxiosInstance) => (data: FormData) => { - return request.post>( - '/api/account/enterpriseRealNameAuth', - data, - { - headers: { - 'Content-Type': 'multipart/form-data' - } - } +export const _enterpriseRealNameAuthPaymentRequest = + (request: AxiosInstance) => + (data: { + key: string; + accountBank: string; + accountNo: string; + keyName: string; + usrName: string; + contactInfo: string; + }) => { + return request.post>( + '/api/account/enterpriseRealName', + data + ); + }; + +export const _enterpriseRealNameAuthVerifyRequest = + (request: AxiosInstance) => (data: { transAmt: string }) => { + return request.post< + typeof data, + ApiResp<{ authState: 'success' | 'failed'; enterpriseRealName: string }> + >('/api/account/enterpriseRealNameVerify', data); + }; + +export const _enterpriseRealNameAuthInfoRequest = (request: AxiosInstance) => () => { + return request.get>('/api/account/enterpriseRealName'); +}; + +export const _enterpriseRealNameAuthCancelRequest = (request: AxiosInstance) => () => { + return request.patch>( + '/api/account/enterpriseRealName' ); }; export const _getAmount = (request: AxiosInstance) => () => request>('/api/account/getAmount'); - +export const _verifyToken = (request: AxiosInstance) => () => + request>('/api/auth/verify'); export const passwordExistRequest = _passwordExistRequest(request); export const passwordLoginRequest = _passwordLoginRequest(request, (token) => { useSessionStore.setState({ token }); }); + export const passwordModifyRequest = _passwordModifyRequest(request); export const UserInfo = _UserInfo(request); +export const verifyToken = _verifyToken(request); export const regionList = _regionList(request); export const getSmsBindCodeRequest = _getSmsBindCodeRequest(request); @@ -211,7 +236,11 @@ export const deleteUserRequest = _deleteUser(request); export const checkRemainResource = _checkRemainResource(request); export const forceDeleteUser = _forceDeleteUser(request); -export const enterpriseRealNameAuthRequest = _enterpriseRealNameAuthRequest(request); +export const enterpriseRealNameAuthPaymentRequest = _enterpriseRealNameAuthPaymentRequest(request); +export const enterpriseRealNameAuthVerifyRequest = _enterpriseRealNameAuthVerifyRequest(request); +export const enterpriseRealNameAuthInfoRequest = _enterpriseRealNameAuthInfoRequest(request); +export const enterpriseRealNameAuthCancelRequest = _enterpriseRealNameAuthCancelRequest(request); + export const faceAuthGenerateQRcodeUriRequest = _faceAuthGenerateQRcodeUriRequest(request); export const getFaceAuthStatusRequest = _getFaceAuthStatusRequest(request); diff --git a/frontend/desktop/src/api/platform.ts b/frontend/desktop/src/api/platform.ts index b37d0347a529..313000b532f3 100644 --- a/frontend/desktop/src/api/platform.ts +++ b/frontend/desktop/src/api/platform.ts @@ -92,6 +92,7 @@ export const getResource = () => { totalMemory: string; totalStorage: string; runningPodCount: string; + totalGpuCount: string; totalPodCount: string; }> >('/api/desktop/getResource'); diff --git a/frontend/desktop/src/components/account/AccountCenter/index.tsx b/frontend/desktop/src/components/account/AccountCenter/index.tsx index 0f311a0c1f4e..c100b5fd22b8 100644 --- a/frontend/desktop/src/components/account/AccountCenter/index.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/index.tsx @@ -222,14 +222,34 @@ export default function Index(props: Omit) { {t('common:realname_info')}} RightElement={ - infoData.data.enterpriseVerificationStatus === 'Success' || - infoData.data.realName ? ( - + infoData.data.enterpriseRealName || infoData.data.realName ? ( + + + + {infoData?.data.enterpriseRealName || infoData?.data.realName} @@ -240,8 +260,37 @@ export default function Index(props: Omit) { _active={{ transform: 'scale(0.95)' }} colorScheme="red" onClick={() => setPageState(PageState.REALNAME_AUTH)} + display="flex" + padding="4px 4px 4px 8px" + justifyContent="center" + alignItems="center" + gap="2px" + borderRadius="6px" + bg="var(--Red-50, #FEF3F2)" + color="var(--Red-500, #F04438)" + fontFamily="PingFang SC" + fontSize="14px" + fontStyle="normal" + fontWeight="500" + lineHeight="20px" + letterSpacing="0.1px" + textTransform="none" > - {t('common:no_realname_auth')} + {t('common:no_realname_auth')} + + + ) } diff --git a/frontend/desktop/src/components/account/RealNameModal.tsx b/frontend/desktop/src/components/account/RealNameModal.tsx index dfc40a332ad2..8c01662c911d 100644 --- a/frontend/desktop/src/components/account/RealNameModal.tsx +++ b/frontend/desktop/src/components/account/RealNameModal.tsx @@ -20,26 +20,20 @@ import { Spinner, Link, FormErrorMessage, - HStack, - TabIndicator, FlexProps } from '@chakra-ui/react'; import { CloseIcon, useMessage, WarningIcon } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; -import React, { - forwardRef, - ReactElement, - useCallback, - useEffect, - useImperativeHandle, - useState -} from 'react'; +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { - enterpriseRealNameAuthRequest, + enterpriseRealNameAuthCancelRequest, + enterpriseRealNameAuthInfoRequest, + enterpriseRealNameAuthPaymentRequest, + enterpriseRealNameAuthVerifyRequest, faceAuthGenerateQRcodeUriRequest, getFaceAuthStatusRequest } from '@/api/auth'; @@ -47,9 +41,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import useSessionStore from '@/stores/session'; import { useQuery } from '@tanstack/react-query'; import QRCode from 'qrcode.react'; -import { useDropzone } from 'react-dropzone'; import { useConfigStore } from '@/stores/config'; -import { DeleteIcon, PictureIcon, UploadIcon, AttachmentIcon } from '../icons'; +import { PAYMENTSTATUS } from '@/types/response/enterpriseRealName'; export function useRealNameAuthNotification(props?: UseToastOptions) { const { t } = useTranslation(); @@ -81,7 +74,6 @@ export function useRealNameAuthNotification(props?: UseToastOptions) { color="yellow.600" fontSize="16px" fontWeight={500} - fontFamily="PingFang SC" lineHeight="24px" letterSpacing="0.15px" fontStyle="normal" @@ -111,21 +103,21 @@ export function useRealNameAuthNotification(props?: UseToastOptions) { - {t('common:realname_auth_reminder_desc', { - reward: realNameReward - })} + {realNameReward?.toString() === '0' + ? t('common:realname_auth_reminder_desc_no_reward') + : t('common:realname_auth_reminder_desc', { + reward: realNameReward + })} {t('common:realName_verification')} - + - + {t('common:personal_verification')} - {/* {t('common:enterprise_verification')} - */} + - - + { @@ -242,7 +273,7 @@ const RealNameModal = forwardRef< }} /> - {/* + { onClose(); @@ -251,7 +282,7 @@ const RealNameModal = forwardRef< } }} /> - */} + @@ -269,36 +300,72 @@ export function RealNameAuthForm( const { t } = useTranslation(); return ( - + {t('common:personal_verification')} - {/* {t('common:enterprise_verification')} - */} + - - + { @@ -308,7 +375,7 @@ export function RealNameAuthForm( }} /> - {/* + { if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { @@ -316,7 +383,7 @@ export function RealNameAuthForm( } }} /> - */} + ); @@ -424,7 +491,7 @@ export function FaceIdRealNameAuthORcode( if (error) { return ( - + {t('common:failed_to_get_qr_code')} @@ -435,7 +502,7 @@ export function FaceIdRealNameAuthORcode( if (isLoading) { return ( -
+
{t('common:loading')}
@@ -443,13 +510,12 @@ export function FaceIdRealNameAuthORcode( } return ( - + {data?.data?.url && ( <>
- {isPolling && ( - - {t('common:waiting_for_face_recognition')} - - )}
)} @@ -483,420 +535,746 @@ export function FaceIdRealNameAuthORcode( ); } -function FileUploadBox({ - onDrop, - file, - removeFile, - label, - description, - isAttachment -}: { - onDrop: (acceptedFiles: File[]) => void; - file: File | null; - removeFile: () => void; - label: string; - description: string; - isAttachment?: boolean; -}) { - const { commonConfig } = useConfigStore((s) => s); - const enterpriseSupportingMaterialsUri = commonConfig?.enterpriseSupportingMaterials; - const { getRootProps, getInputProps } = useDropzone({ - onDrop, - maxFiles: 1, - accept: { - 'image/*': ['.png', '.jpg', '.jpeg'], - 'application/pdf': ['.pdf'] - } - }); - const { t } = useTranslation(); - - return ( - - - {label} - - - - {description} - - - - - - - {t('common:click_to_upload_file')} - - - {' '} - - - {file ? ( - - - - - {file.name} - - - - - ) : null} - - {isAttachment && ( - - - - - {t('common:attachment')} - - - - )} - - - - ); -} - function EnterpriseVerification( props: FlexProps & { onFormSuccess?: () => void; } ) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { message } = useMessage(); - const { session } = useSessionStore((s) => s); const { setSessionProp } = useSessionStore(); - const queryClient = useQueryClient(); + const domain = useConfigStore((state) => state.cloudConfig?.domain); const schema = z.object({ - enterpriseName: z + key: z.string().min(1, { message: t('common:enterprise_key_required') }), + accountBank: z.string().min(1, { message: t('common:account_bank_required') }), + accountNo: z.string().min(1, { message: t('common:account_number_required') }), + keyName: z.string().min(1, { message: t('common:enterprise_name_required') }), + usrName: z.string().min(1, { message: t('common:user_name_required') }), + contactInfo: z + .string() + .min(1, { message: t('common:contact_info_required') }) + .regex(/^\d+$/, { message: t('common:contact_info_must_be_numeric') }) + }); + + const verificationSchema = z.object({ + transAmt: z .string() - .min(1, { message: t('common:enterprise_name_required') }) - .max(50, { message: t('common:enterprise_name_required') }), - enterpriseQualification: z.instanceof(File).nullable(), - supportingMaterials: z.instanceof(File).nullable() + .refine((val) => /^\d{1,3}$/.test(val), { message: t('common:invalid_verification_amount') }) }); + const { data: enterpriseRealNameAuthInfo, isLoading: enterpriseRealNameAuthInfoLoading } = + useQuery(['enterpriseRealNameAuthInfo'], enterpriseRealNameAuthInfoRequest, { + refetchOnWindowFocus: false + }); + type FormData = z.infer; const { - register, - handleSubmit, - setValue, - watch, - reset, - formState: { errors } - } = useForm({ + register: registerMain, + handleSubmit: handleMainSubmit, + reset: resetMain, + formState: { errors: mainErrors } + } = useForm>({ resolver: zodResolver(schema), - defaultValues: { - enterpriseName: '', - enterpriseQualification: null, - supportingMaterials: null - } + mode: 'onChange' }); - const enterpriseQualification = watch('enterpriseQualification'); - const supportingMaterials = watch('supportingMaterials'); + const { + register: registerVerification, + watch: watchVerification, + reset: resetVerification, + formState: { errors: verificationErrors } + } = useForm>({ + resolver: zodResolver(verificationSchema), + mode: 'onChange' + }); - const onDropEnterpriseQualification = useCallback( - (acceptedFiles: File[]) => { - setValue('enterpriseQualification', acceptedFiles[0], { shouldValidate: true }); - }, - [setValue] - ); + const transAmt = watchVerification('transAmt'); - const onDropCertificateMaterial = useCallback( - (acceptedFiles: File[]) => { - setValue('supportingMaterials', acceptedFiles[0], { shouldValidate: true }); - }, - [setValue] - ); + useEffect(() => { + if ( + enterpriseRealNameAuthInfo?.data && + enterpriseRealNameAuthInfo.data.paymentStatus === PAYMENTSTATUS.PROCESSING + ) { + const { key, accountBank, accountNo, keyName, usrName, contactInfo } = + enterpriseRealNameAuthInfo.data; + resetMain({ + key, + accountBank, + accountNo, + keyName, + usrName, + contactInfo + }); + } + }, [enterpriseRealNameAuthInfo?.data, resetMain]); - const removeEnterpriseQualification = () => - setValue('enterpriseQualification', null, { shouldValidate: true }); - const removeSupportingMaterials = () => - setValue('supportingMaterials', null, { shouldValidate: true }); + const canPayment = enterpriseRealNameAuthInfo?.data?.paymentStatus !== PAYMENTSTATUS.PROCESSING; - const enterpriseRealNameAuthMutation = useMutation(enterpriseRealNameAuthRequest, { + const canVerify = enterpriseRealNameAuthInfo?.data?.paymentStatus === PAYMENTSTATUS.PROCESSING; + + const canCancel = enterpriseRealNameAuthInfo?.data?.paymentStatus === PAYMENTSTATUS.PROCESSING; + + const canInput = enterpriseRealNameAuthInfo?.data?.paymentStatus !== PAYMENTSTATUS.PROCESSING; + + const remainingAttempts = enterpriseRealNameAuthInfo?.data?.remainingAttempts; + + const [errorMessage, setErrorMessage] = useState(''); + + const enterpriseRealNameAuthPaymentMutation = useMutation(enterpriseRealNameAuthPaymentRequest, { onSuccess: (data) => { - if (data.code === 200) { + if (data.code === 200 && data.data?.paymentStatus === PAYMENTSTATUS.PROCESSING) { message({ - title: t('common:upload_success'), + title: t('common:enterprise_realname_payment_success'), status: 'success', duration: 2000, - isClosable: true, - position: 'top' + isClosable: true }); + setErrorMessage(''); + queryClient.invalidateQueries(['enterpriseRealNameAuthInfo']); + } else { + message({ + title: t('common:enterprise_realname_payment_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(data.message || t('common:enterprise_realname_payment_failed')); + } + }, + onError: (error: any) => { + message({ + title: t('common:enterprise_realname_payment_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(error.message || t('common:enterprise_realname_payment_failed')); + } + }); + + const enterpriseRealNameAuthVerifyMutation = useMutation(enterpriseRealNameAuthVerifyRequest, { + onSuccess: (data) => { + if (data.code === 200 && data.data?.authState === 'success') { + message({ + title: t('common:enterprise_realname_verify_success'), + status: 'success', + duration: 2000, + isClosable: true + }); + + setErrorMessage(''); - queryClient.invalidateQueries([session?.token, 'UserInfo']); setSessionProp('user', { ...useSessionStore.getState().session!.user!, - enterpriseVerificationStatus: data.data?.status + enterpriseRealName: data.data?.enterpriseRealName }); - reset(); + queryClient.invalidateQueries([session?.token, 'UserInfo']); + queryClient.invalidateQueries(['enterpriseRealNameAuthInfo']); + resetMain(); + resetVerification(); - if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + if (props.onFormSuccess) { props.onFormSuccess(); } } else { message({ - title: data.message, + title: t('common:enterprise_realname_verify_failed'), status: 'error', - position: 'top', duration: 2000, isClosable: true }); + setErrorMessage( + data.message ? t(data.message as any) : t('common:enterprise_realname_verify_failed') + ); } }, - onError: (error: Error) => { + onError: (error: any) => { message({ - title: error.message, + title: t('common:enterprise_realname_verify_failed'), status: 'error', - position: 'top', duration: 2000, isClosable: true }); + setErrorMessage(error.message || t('common:enterprise_realname_verify_failed')); } }); - const onValidate = async (data: { - enterpriseName: string; - enterpriseQualification: File | null; - supportingMaterials: File | null; - }) => { - if (!data.enterpriseQualification || !data.supportingMaterials) { + const enterpriseRealNameAuthCancelMutation = useMutation(enterpriseRealNameAuthCancelRequest, { + onSuccess: (data) => { + if (data.code === 200 && data.data?.paymentStatus === PAYMENTSTATUS.CANCEL) { + message({ + title: t('common:enterprise_realname_cancel_success'), + status: 'success', + duration: 2000, + isClosable: true + }); + setErrorMessage(''); + queryClient.invalidateQueries(['enterpriseRealNameAuthInfo']); + } else { + message({ + title: t('common:enterprise_realname_cancel_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(t('common:enterprise_realname_cancel_failed')); + } + }, + onError: (error: any) => { message({ - title: t('common:please_fill_all_fields'), - status: 'warning', - position: 'top', + title: t('common:enterprise_realname_cancel_failed'), + status: 'error', duration: 2000, isClosable: true }); - - return; + setErrorMessage(error.message || t('common:enterprise_realname_cancel_failed')); } - const formData = new FormData(); - formData.append('enterpriseName', data.enterpriseName); - formData.append('enterpriseQualification', data.enterpriseQualification); - formData.append('supportingMaterials', data.supportingMaterials); - enterpriseRealNameAuthMutation.mutate(formData); - }; + }); - const onInvalid = () => { - const firstErrorMessage = Object.values(errors)[0]?.message; - if (firstErrorMessage) { + const handleVerifyClick = () => { + if (!transAmt || !/^\d{1,3}$/.test(transAmt)) { message({ - title: firstErrorMessage, + title: t('common:invalid_verification_amount'), status: 'error', - position: 'top', duration: 2000, isClosable: true }); + setErrorMessage(t('common:invalid_verification_amount')); + return; } + + enterpriseRealNameAuthVerifyMutation.mutate({ transAmt }); }; - const onSubmit = handleSubmit(onValidate, onInvalid); + if (enterpriseRealNameAuthInfoLoading) { + return ( +
+ + {t('common:loading')} +
+ ); + } return ( -
- + { + enterpriseRealNameAuthPaymentMutation.mutate(data); + })} > - - + + {t('common:enterprise_auth_tips')} + + + + + + + {t('common:enterprise_keyname')} + + + + + {mainErrors.keyName?.message} + + + + + + + {t('common:enterprise_key')} + + + + + {mainErrors.key?.message} + + + + + + + {t('common:legal_person')} + + + + + {mainErrors.usrName?.message} + + + + + + + {t('common:account_number')} + + + + + {mainErrors.accountNo?.message} + + + + + + + {t('common:bank_name')} + + + + + {mainErrors.accountBank?.message} + + + + + + + {t('common:contact_info')} + + + + + {mainErrors.contactInfo?.message} + + + + {(errorMessage || !canPayment) && ( + - - {t('common:enterprise_name')} - - - - {errors.enterpriseName && ( - {errors.enterpriseName.message} - )} - - - - - - - - - - + + + + + {verificationErrors.transAmt?.message} + + + + {t('common:verification_amount_tips')} + + {t('common:link_to_workorder')} + + {i18n.language === 'zh' && '。'} + + + + + + + + - -
+
+ ); } diff --git a/frontend/desktop/src/components/desktop_content/index.tsx b/frontend/desktop/src/components/desktop_content/index.tsx index 345a330c6e83..173cb4c455cb 100644 --- a/frontend/desktop/src/components/desktop_content/index.tsx +++ b/frontend/desktop/src/components/desktop_content/index.tsx @@ -127,7 +127,7 @@ export default function Desktop(props: any) { useEffect(() => { if (infoData.isSuccess && commonConfig?.realNameAuthEnabled) { - if (!infoData?.data?.realName && infoData?.data?.enterpriseVerificationStatus !== 'Success') { + if (!infoData?.data?.realName && !infoData?.data?.enterpriseRealName) { realNameAuthNotificationIdRef.current = realNameAuthNotification({ duration: null, isClosable: true diff --git a/frontend/desktop/src/components/desktop_content/monitor.tsx b/frontend/desktop/src/components/desktop_content/monitor.tsx index 53c7d0b791a0..5cbeaf58d5a1 100644 --- a/frontend/desktop/src/components/desktop_content/monitor.tsx +++ b/frontend/desktop/src/components/desktop_content/monitor.tsx @@ -3,8 +3,9 @@ import { Box, CircularProgress, CircularProgressLabel, Flex, Text } from '@chakr import { MonitorIcon } from '@sealos/ui'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; -import { CpuIcon, FlowIcon, MemoryIcon, StorageIcon } from '../icons'; +import { CpuIcon, FlowIcon, GpuIcon, MemoryIcon, StorageIcon } from '../icons'; import { blurBackgroundStyles } from './index'; +import { useMemo } from 'react'; export default function Monitor({ needStyles = true }: { needStyles?: boolean }) { const { t } = useTranslation(); @@ -12,32 +13,45 @@ export default function Monitor({ needStyles = true }: { needStyles?: boolean }) staleTime: 60 * 1000 }); - const info = [ - { - label: 'CPU', - value: data?.data?.totalCpu, - icon: , - unit: 'C' - }, - { - label: t('common:memory'), - value: data?.data?.totalMemory, - icon: , - unit: 'GB' - }, - { - label: t('common:storage'), - value: data?.data?.totalStorage, - icon: , - unit: 'GB' - }, - { - label: t('common:flow'), - value: `~`, - icon: , - unit: 'GB' - } - ]; + const info = useMemo( + () => [ + { + label: 'CPU', + value: data?.data?.totalCpu, + icon: , + unit: 'C' + }, + { + label: t('common:memory'), + value: data?.data?.totalMemory, + icon: , + unit: 'GB' + }, + { + label: t('common:storage'), + value: data?.data?.totalStorage, + icon: , + unit: 'GB' + }, + { + label: t('common:flow'), + value: `~`, + icon: , + unit: 'GB' + }, + ...(Number(data?.data?.totalGpuCount) > 0 + ? [ + { + label: 'GPU', + value: data?.data?.totalGpuCount, + icon: , + unit: 'Card' + } + ] + : []) + ], + [data?.data, t] + ); const totalPodCount = Number(data?.data?.totalPodCount) || 0; const runningPodCount = Number(data?.data?.runningPodCount) || 0; diff --git a/frontend/desktop/src/components/icons/index.tsx b/frontend/desktop/src/components/icons/index.tsx index 5e42b63ca27f..54aa7322e449 100644 --- a/frontend/desktop/src/components/icons/index.tsx +++ b/frontend/desktop/src/components/icons/index.tsx @@ -605,3 +605,24 @@ export function AttachmentIcon(props: IconProps) { ); } + +export function GpuIcon(props: IconProps) { + return ( + + + + + + ); +} diff --git a/frontend/desktop/src/components/signin/auth/usePassword.tsx b/frontend/desktop/src/components/signin/auth/usePassword.tsx index be8ed7cecf75..b95c4604b3ec 100644 --- a/frontend/desktop/src/components/signin/auth/usePassword.tsx +++ b/frontend/desktop/src/components/signin/auth/usePassword.tsx @@ -95,8 +95,6 @@ export default function usePassword({ userId: payload.userId, userUid: payload.userUid, realName: infoData.data?.info.realName || undefined, - enterpriseVerificationStatus: - infoData.data?.info.enterpriseVerificationStatus || undefined, enterpriseRealName: infoData.data?.info.enterpriseRealName || undefined, userRestrictedLevel: infoData.data?.info.userRestrictedLevel || undefined }, diff --git a/frontend/desktop/src/components/signin/auth/useWechat.tsx b/frontend/desktop/src/components/signin/auth/useWechat.tsx index ea2084389d24..759830f2d863 100644 --- a/frontend/desktop/src/components/signin/auth/useWechat.tsx +++ b/frontend/desktop/src/components/signin/auth/useWechat.tsx @@ -54,8 +54,6 @@ export default function useWechat() { userId: payload.userId, realName: infoData.data?.info.realName || undefined, userRestrictedLevel: infoData.data?.info.userRestrictedLevel || undefined, - enterpriseVerificationStatus: - infoData.data?.info.enterpriseVerificationStatus || undefined, enterpriseRealName: infoData.data?.info.enterpriseRealName || undefined }, // @ts-ignore diff --git a/frontend/desktop/src/components/task/useDriver.tsx b/frontend/desktop/src/components/task/useDriver.tsx index b18967179415..1a1ddbca9a97 100644 --- a/frontend/desktop/src/components/task/useDriver.tsx +++ b/frontend/desktop/src/components/task/useDriver.tsx @@ -1,6 +1,7 @@ import { checkUserTask, getUserTasks, updateTask } from '@/api/platform'; import { AppStoreIcon, DBproviderIcon, DriverStarIcon, LaunchpadIcon } from '@/components/icons'; import useAppStore from '@/stores/app'; +import useCallbackStore from '@/stores/callback'; import { useConfigStore } from '@/stores/config'; import { useDesktopConfigStore } from '@/stores/desktopConfig'; import { UserTask } from '@/types/task'; @@ -22,6 +23,7 @@ export default function useDriver() { const { taskComponentState, setTaskComponentState } = useDesktopConfigStore(); const { canShowGuide } = useDesktopConfigStore(); const { installedApps } = useAppStore(); + const { workspaceInviteCode } = useCallbackStore(); useEffect(() => { const fetchUserTasks = async () => { @@ -51,13 +53,13 @@ export default function useDriver() { } }; - if (isPC && conf?.guideEnabled && canShowGuide) { + if (isPC && conf?.guideEnabled && canShowGuide && !workspaceInviteCode) { handleUserGuide(); } else { setDesktopGuide(false); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [conf?.guideEnabled, isPC, canShowGuide]); + }, [conf?.guideEnabled, isPC, canShowGuide, workspaceInviteCode]); const completeGuide = async () => { try { diff --git a/frontend/desktop/src/components/team/WorkspaceToggle.tsx b/frontend/desktop/src/components/team/WorkspaceToggle.tsx index c9b02ef69bf4..81136db5346e 100644 --- a/frontend/desktop/src/components/team/WorkspaceToggle.tsx +++ b/frontend/desktop/src/components/team/WorkspaceToggle.tsx @@ -1,7 +1,6 @@ import { nsListRequest, switchRequest } from '@/api/namespace'; import NsListItem from '@/components/team/NsListItem'; import TeamCenter from '@/components/team/TeamCenter'; -import useAppStore from '@/stores/app'; import useSessionStore from '@/stores/session'; import { NSType } from '@/types/team'; import { AccessTokenPayload } from '@/types/token'; @@ -16,13 +15,12 @@ import { CubeIcon, DesktopExchangeIcon } from '../icons'; export default function WorkspaceToggle() { const disclosure = useDisclosure(); - const { setWorkSpaceId, session } = useSessionStore(); + const { session } = useSessionStore(); const { t } = useTranslation(); const user = session?.user; const ns_uid = user?.ns_uid || ''; const router = useRouter(); const queryClient = useQueryClient(); - const { init } = useAppStore(); const mutation = useMutation({ mutationFn: switchRequest, async onSuccess(data) { diff --git a/frontend/desktop/src/constants/account.ts b/frontend/desktop/src/constants/account.ts index 0ccc4f735e71..c41c44de088e 100644 --- a/frontend/desktop/src/constants/account.ts +++ b/frontend/desktop/src/constants/account.ts @@ -4,3 +4,8 @@ export const LicenseFrontendKey = 'cloud.sealos.io/license-frontend'; export const templateDeployKey = 'cloud.sealos.io/deploy-on-sealos'; export const userSystemNamespace = 'user-system' as const; + +export enum trackEventName { + 'dailyLoginFirst' = 'dailyLoginFirst', + 'signUp' = 'signUp' +} diff --git a/frontend/desktop/src/pages/_app.tsx b/frontend/desktop/src/pages/_app.tsx index 2205a6583b4a..a5847777351c 100644 --- a/frontend/desktop/src/pages/_app.tsx +++ b/frontend/desktop/src/pages/_app.tsx @@ -48,4 +48,5 @@ const App = ({ Component, pageProps }: AppProps) => { ); }; + export default appWithTranslation(App); diff --git a/frontend/desktop/src/pages/_document.tsx b/frontend/desktop/src/pages/_document.tsx index 91869f08cfe1..1a1c9f10283b 100644 --- a/frontend/desktop/src/pages/_document.tsx +++ b/frontend/desktop/src/pages/_document.tsx @@ -1,6 +1,6 @@ -import { Head, Html, Main, NextScript } from 'next/document'; -import { ColorModeScript } from '@chakra-ui/react'; import { theme } from '@/styles/chakraTheme'; +import { ColorModeScript } from '@chakra-ui/react'; +import { Head, Html, Main, NextScript } from 'next/document'; export default function Document() { return ( diff --git a/frontend/desktop/src/pages/api/account/enterpriseRealName.ts b/frontend/desktop/src/pages/api/account/enterpriseRealName.ts new file mode 100644 index 000000000000..b82a2ab6dea8 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/enterpriseRealName.ts @@ -0,0 +1,337 @@ +import { jsonRes } from '@/services/backend/response'; +import { enableEnterpriseRealNameAuth } from '@/services/enable'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { generateAuthenticationToken, verifyAccessToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { z } from 'zod'; +import { PAYMENTSTATUS } from '@/types/response/enterpriseRealName'; +import { EnterpriseAuthInfo } from '@/types/response/enterpriseRealName'; +import { AdditionalInfo } from '@/types/response/enterpriseRealName'; +import { AccessTokenPayload } from '@/types/token'; + +type JsonValue = string | number | boolean | object | null; + +type RealNameAuthProvider = { + id: string; + backend: string; + authType: string; + maxFailedTimes: number; + config: JsonValue; + createdAt: Date; + updatedAt: Date; +}; + +type UnionPay3060Config = { + api: string; +}; + +interface ApiError { + detail: any; + errorId: string; + message: string; + timestamp: number; + code: string; +} + +interface EnterpriseAuthResponse { + subBank: string; + transAmt: string; + orderId: string; + isCharged: boolean; + legalPersonName: string; + isTransactionSuccess: boolean; + key: string; + enterpriseName: string; + accountCity: string; + respCode: string; + respMsg: string; + accountProv: string; + accountBank: string; + accountNo: string; +} + +interface ApiResponse { + error?: ApiError; + data?: EnterpriseAuthResponse; + success: boolean; +} + +const schema = z.object({ + key: z.string().min(1), + accountBank: z.string().min(1), + accountNo: z.string().min(1), + keyName: z.string().min(1), + usrName: z.string().min(1), + contactInfo: z.string().min(1).regex(/^\d+$/, 'Contact info must contain only numbers') +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableEnterpriseRealNameAuth) { + console.error('enterpriseRealNameAuth: enterprise real name authentication not enabled'); + return jsonRes(res, { code: 503, message: 'Enterprise real name authentication not enabled' }); + } + + const payload = await verifyAccessToken(req.headers); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invalid' }); + + switch (req.method) { + case 'GET': + return handleGet(req, res, payload.userUid); + case 'POST': + return handlePost(req, res, payload.userUid, payload); + case 'PATCH': + return handlePatch(req, res, payload.userUid); + default: + console.error('enterpriseRealNameAuth: Method not allowed'); + return jsonRes(res, { code: 405, message: 'Method not allowed' }); + } +} + +async function handleGet(req: NextApiRequest, res: NextApiResponse, userUid: string) { + try { + const realNameAuthProvider: RealNameAuthProvider | null = + await globalPrisma.realNameAuthProvider.findFirst({ + where: { + backend: 'UNIONPAY', + authType: '3060' + } + }); + + if (!realNameAuthProvider) { + throw new Error('enterpriseRealNameAuth: Real name authentication provider not found'); + } + + const info = await globalPrisma.enterpriseRealNameInfo.findUnique({ + where: { userUid } + }); + + if (!info || !info.additionalInfo) { + return jsonRes(res, { + code: 200, + data: { remainingAttempts: realNameAuthProvider.maxFailedTimes } + }); + } + + const additionalInfo = info.additionalInfo as unknown as AdditionalInfo; + + const response: EnterpriseAuthInfo = { + paymentStatus: additionalInfo.paymentStatus, + key: additionalInfo.key, + accountBank: additionalInfo.accountBank, + accountNo: additionalInfo.accountNo, + keyName: additionalInfo.keyName, + usrName: additionalInfo.usrName, + contactInfo: additionalInfo.contactInfo, + remainingAttempts: realNameAuthProvider.maxFailedTimes - (additionalInfo.authTimes || 0) + }; + + return jsonRes(res, { code: 200, data: response }); + } catch (error) { + console.error('Error fetching enterprise auth info:', error); + return jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} + +async function handlePost( + req: NextApiRequest, + res: NextApiResponse, + userUid: string, + payload: AccessTokenPayload +) { + try { + const realNameAuthProvider: RealNameAuthProvider | null = + await globalPrisma.realNameAuthProvider.findFirst({ + where: { + backend: 'UNIONPAY', + authType: '3060' + } + }); + + if (!realNameAuthProvider) { + throw new Error('enterpriseRealNameAuth: Real name authentication provider not found'); + } + + const config: UnionPay3060Config = realNameAuthProvider.config as UnionPay3060Config; + + if (!config) { + throw new Error('enterpriseRealNameAuth: Real name authentication configuration not found'); + } + + const enterpriseRealNameAuthApi = config.api; + + const enterpriseRealName = await globalPrisma.enterpriseRealNameInfo.findUnique({ + where: { userUid } + }); + + if (enterpriseRealName && enterpriseRealName.isVerified) { + return jsonRes(res, { + code: 400, + message: 'Enterprise real name authentication has been completed' + }); + } + + if ( + enterpriseRealName && + enterpriseRealName.additionalInfo && + (enterpriseRealName.additionalInfo as unknown as AdditionalInfo).authTimes >= + realNameAuthProvider.maxFailedTimes + ) { + return jsonRes(res, { + code: 400, + message: 'Enterprise real name authentication has reached the maximum number of attempts' + }); + } + + const validationResult = schema.safeParse(req.body); + if (!validationResult.success) { + return jsonRes(res, { + code: 400, + message: 'Invalid request body', + data: validationResult.error.issues + }); + } + + const data = validationResult.data; + + const enterprise = await globalPrisma.enterpriseRealNameInfo.findFirst({ + where: { + enterpriseName: data.keyName, + isVerified: true + } + }); + + if (enterprise) { + return jsonRes(res, { + code: 400, + message: 'Enterprise real name information has been used' + }); + } + + const globalToken = generateAuthenticationToken({ + userUid: payload.userUid, + userId: payload.userId, + regionUid: payload.regionUid + }); + + const response = await fetch(enterpriseRealNameAuthApi, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${globalToken}` + }, + cache: 'no-store', + body: JSON.stringify({ + key: data.key, + accountBank: data.accountBank, + accountNo: data.accountNo, + keyName: data.keyName, + usrName: data.usrName + }) + }); + + const apiResponse: ApiResponse = await response.json(); + + if (!apiResponse.success) { + return jsonRes(res, { + code: 400, + message: apiResponse?.error?.message || 'API request failed' + }); + } + + if (!apiResponse.data?.isTransactionSuccess || !apiResponse.data?.transAmt) { + return jsonRes(res, { + code: 400, + message: `request failed, code: ${apiResponse.data?.respCode}, message: ${apiResponse.data?.respMsg}` + }); + } + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + await globalPrisma.enterpriseRealNameInfo.upsert({ + where: { userUid }, + update: { + enterpriseName: apiResponse.data.enterpriseName, + isVerified: false, + additionalInfo: { + paymentStatus: PAYMENTSTATUS.PROCESSING, + key: apiResponse.data.key, + accountBank: apiResponse.data.accountBank, + accountNo: apiResponse.data.accountNo, + keyName: apiResponse.data.enterpriseName, + usrName: apiResponse.data.legalPersonName, + contactInfo: data.contactInfo, + transAmt: apiResponse.data.transAmt, + authTimes: (enterpriseRealName?.additionalInfo as unknown as AdditionalInfo)?.authTimes + ? (enterpriseRealName?.additionalInfo as unknown as AdditionalInfo).authTimes + 1 + : 1 + } + }, + create: { + userUid, + enterpriseName: apiResponse.data.enterpriseName, + isVerified: false, + additionalInfo: { + paymentStatus: PAYMENTSTATUS.PROCESSING, + key: apiResponse.data.key, + accountBank: apiResponse.data.accountBank, + accountNo: apiResponse.data.accountNo, + keyName: apiResponse.data.enterpriseName, + usrName: apiResponse.data.legalPersonName, + contactInfo: data.contactInfo, + transAmt: apiResponse.data.transAmt, + authTimes: 1 + } + } + }); + + return jsonRes(res, { + code: 200, + message: 'Enterprise auth request processed successfully', + data: { + paymentStatus: PAYMENTSTATUS.PROCESSING + } + }); + } catch (error) { + console.error('Error processing enterprise auth request:', error); + return jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} + +async function handlePatch(req: NextApiRequest, res: NextApiResponse, userUid: string) { + try { + const info = await globalPrisma.enterpriseRealNameInfo.findUnique({ + where: { userUid } + }); + + if (!info) { + return jsonRes(res, { code: 404, message: 'Enterprise auth info not found' }); + } + + if (info.isVerified) { + return jsonRes(res, { code: 400, message: 'Enterprise auth has been completed' }); + } + + await globalPrisma.enterpriseRealNameInfo.update({ + where: { userUid }, + data: { + additionalInfo: { + ...(info.additionalInfo as object), + paymentStatus: PAYMENTSTATUS.CANCEL + } + } + }); + + return jsonRes(res, { + code: 200, + message: 'Payment status updated successfully', + data: { + paymentStatus: PAYMENTSTATUS.CANCEL + } + }); + } catch (error) { + console.error('Error updating payment status:', error); + return jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} diff --git a/frontend/desktop/src/pages/api/account/enterpriseRealNameAuth.ts b/frontend/desktop/src/pages/api/account/enterpriseRealNameAuth.ts deleted file mode 100644 index dcbb683ad37f..000000000000 --- a/frontend/desktop/src/pages/api/account/enterpriseRealNameAuth.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { jsonRes } from '@/services/backend/response'; -import { enableEnterpriseRealNameAuth } from '@/services/enable'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { verifyAccessToken } from '@/services/backend/auth'; -import { globalPrisma } from '@/services/backend/db/init'; -import { RealNameOSSConfigType } from '@/types'; -import * as Minio from 'minio'; -import formidable, { Fields, Files, File, Part } from 'formidable'; -import path from 'path'; -import Formidable from 'formidable/Formidable'; -import fs from 'fs/promises'; - -export const config = { - api: { - bodyParser: false - } -}; - -const realNameOSS: RealNameOSSConfigType = global.AppConfig.realNameOSS; - -const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB -const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf']; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableEnterpriseRealNameAuth) { - console.error('enterpriseRealNameAuth: enterprise real name authentication not enabled'); - return jsonRes(res, { code: 503, message: 'Enterprise real name authentication not enabled' }); - } - - if (req.method !== 'POST') { - console.error('enterpriseRealNameAuth: Method not allowed'); - return jsonRes(res, { code: 405, message: 'Method not allowed' }); - } - - const payload = await verifyAccessToken(req.headers); - if (!payload) return jsonRes(res, { code: 401, message: 'Token is invalid' }); - - if (!realNameOSS) { - return jsonRes(res, { - code: 500, - message: 'Real name authentication oss configuration not found' - }); - } - - try { - const userUid = payload.userUid; - - const form = formidable({ - multiples: false, - keepExtensions: true, - maxFileSize: MAX_FILE_SIZE, - filter: function (part: Part): boolean { - return part.mimetype !== null && ALLOWED_FILE_TYPES.includes(part.mimetype); - }, - filename: function (name: string, ext: string, part: Part, form: Formidable): string { - const sanitizedName = sanitizeFilename(part.originalFilename || 'unnamed'); - return sanitizedName; - } - }); - - const formData = await parseFormData(req, form); - const { fields, files } = formData; - - const enterpriseName = fields.enterpriseName?.[0]; - - if ((enterpriseName && enterpriseName.length < 1) || enterpriseName.length > 20) { - return jsonRes(res, { - code: 400, - message: 'Enterprise name must be between 1 and 20 characters' - }); - } - - if (!files.enterpriseQualification?.[0] || !files.supportingMaterials?.[0]) { - return jsonRes(res, { - code: 400, - message: 'Enterprise qualification and supporting materials are required' - }); - } - - if ( - files && - files.enterpriseQualification?.[0] && - files.enterpriseQualification?.[0].size > MAX_FILE_SIZE - ) { - return jsonRes(res, { - code: 400, - message: 'Enterprise qualification file size exceeds the maximum limit' - }); - } - - if ( - files && - files.supportingMaterials?.[0] && - files.supportingMaterials?.[0].size > MAX_FILE_SIZE - ) { - return jsonRes(res, { - code: 400, - message: 'Supporting materials file size exceeds the maximum limit' - }); - } - - // Check if EnterpriseRealNameInfo exists - const existingInfo = await globalPrisma.enterpriseRealNameInfo.findUnique({ - where: { userUid } - }); - - if (!existingInfo) { - // Create new EnterpriseRealNameInfo - const ossPaths = await uploadFiles(userUid, files); - await createEnterpriseRealNameInfo(userUid, enterpriseName, ossPaths); - return jsonRes(res, { - code: 200, - message: 'Enterprise real name authentication submitted successfully', - data: { status: 'Pending' } - }); - } - - // Handle existing EnterpriseRealNameInfo cases - switch (existingInfo.verificationStatus) { - case 'Pending': - return jsonRes(res, { code: 400, message: 'Authentication is under review' }); - case 'Success': - return jsonRes(res, { code: 400, message: 'Cannot authenticate multiple times' }); - case 'Failed': - // Re-upload files and update EnterpriseRealNameInfo - const newOssPaths = await uploadFiles(userUid, files); - await updateEnterpriseRealNameInfo(existingInfo.id, enterpriseName, newOssPaths); - return jsonRes(res, { - code: 200, - data: { status: 'Pending' }, - message: 'Enterprise real name authentication resubmitted successfully' - }); - default: - return jsonRes(res, { code: 500, message: 'Invalid verification status' }); - } - } catch (error) { - console.error('enterpriseRealNameAuth: Internal error', error); - return jsonRes(res, { code: 500, message: 'The server has encountered an error' }); - } -} - -// Helper functions -async function parseFormData(req: NextApiRequest, form: Formidable): Promise { - return new Promise((resolve, reject) => { - form.parse(req, (err: Error, fields: Fields, files: Files) => { - if (err) { - reject(err); - } else { - resolve({ fields, files }); - } - }); - }); -} - -function sanitizeFilename(filename: string): string { - // Remove any path components - const basename = path.basename(filename); - // Define a blacklist of malicious characters - const blacklist = /[<>:"/\\|?*\x00-\x1F]/g; - // Replace blacklisted characters with underscores - return basename.replace(blacklist, '_'); -} - -async function uploadFiles(userUid: string, files: Files): Promise { - const minioConfig: Minio.ClientOptions = { - endPoint: realNameOSS.endpoint, - accessKey: realNameOSS.accessKey, - secretKey: realNameOSS.accessKeySecret, - useSSL: realNameOSS.ssl - }; - const minioClient = new Minio.Client(minioConfig); - - const filesToUpload = [ - { file: files.enterpriseQualification?.[0], name: 'enterpriseQualification' }, - { file: files.supportingMaterials?.[0], name: 'supportingMaterials' } - ].filter((item) => item.file !== null); - - const uploadPromises = filesToUpload.map((item) => - uploadFile(minioClient, userUid, item.file!, item.name) - ); - - return await Promise.all(uploadPromises); -} - -async function uploadFile( - minioClient: Minio.Client, - userUid: string, - file: File, - fileType: string -): Promise { - const timestamp = Date.now(); - const fileName = `${timestamp}_${fileType}_${file.newFilename}`; - const filePath = `/${userUid}/${fileName}`; - try { - await minioClient.fPutObject(realNameOSS.enterpriseRealNameBucket, filePath, file.filepath, { - 'Content-Type': file.mimetype || 'application/octet-stream' - }); - - // Check if the file exists before attempting to delete it - try { - await fs.access(file.filepath); - // If no error is thrown, the file exists, so we can delete it - await fs.unlink(file.filepath); - console.debug(`File ${file.filepath} has been deleted.`); - } catch (accessError) { - // If an error is thrown, the file doesn't exist - console.debug(`File ${file.filepath} does not exist or is not accessible.`); - } - } catch (error) { - console.error('EnterpriseRealNameAuth uploadFile: Error uploading file', error); - throw error; - } - - return filePath; -} - -async function createEnterpriseRealNameInfo( - userUid: string, - enterpriseName: string, - ossPaths: string[] -) { - await globalPrisma.enterpriseRealNameInfo.create({ - data: { - userUid, - enterpriseName: enterpriseName, - supportingMaterials: { ossPaths: ossPaths }, // Remaining paths for supportingMaterials - verificationStatus: 'Pending' - } - }); -} - -async function updateEnterpriseRealNameInfo( - id: string, - enterpriseName: string, - ossPaths: string[] -) { - await globalPrisma.enterpriseRealNameInfo.update({ - where: { id }, - data: { - enterpriseName: enterpriseName, - supportingMaterials: { ossPaths: ossPaths }, - verificationStatus: 'Pending' - } - }); -} diff --git a/frontend/desktop/src/pages/api/account/enterpriseRealNameVerify.ts b/frontend/desktop/src/pages/api/account/enterpriseRealNameVerify.ts new file mode 100644 index 000000000000..bf0057224f50 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/enterpriseRealNameVerify.ts @@ -0,0 +1,248 @@ +import { jsonRes } from '@/services/backend/response'; +import { enableEnterpriseRealNameAuth } from '@/services/enable'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { z } from 'zod'; +import { PAYMENTSTATUS } from '@/types/response/enterpriseRealName'; +import { AdditionalInfo } from '@/types/response/enterpriseRealName'; +import { RealNameAuthProvider } from '@/pages/api/account/faceIdRealNameAuthCallback'; +import { TencentCloudFaceAuthConfig } from '@/pages/api/account/faceIdRealNameAuthCallback'; +import { getInviterInfo } from '@/utils/getInviteInfo'; + +const schema = z.object({ + transAmt: z.string().min(1) +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableEnterpriseRealNameAuth) { + console.error('enterpriseRealNameVerify: enterprise real name authentication not enabled'); + return jsonRes(res, { code: 503, message: 'Enterprise real name authentication not enabled' }); + } + + if (req.method !== 'POST') { + console.error('enterpriseRealNameVerify: Method not allowed'); + return jsonRes(res, { code: 405, message: 'Method not allowed' }); + } + + const payload = await verifyAccessToken(req.headers); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invalid' }); + + const userUid = payload.userUid; + const userId = payload.userId; + + const inviteInfo = await getInviterInfo(userId); + + try { + const realNameAuthProvider: RealNameAuthProvider | null = + await globalPrisma.realNameAuthProvider.findFirst({ + where: { + backend: 'TENCENTCLOUD', + authType: 'tcloudFaceAuth' + } + }); + + if (!realNameAuthProvider) { + throw new Error('faceidRealNameAuth: Real name authentication provider not found'); + } + + const config: TencentCloudFaceAuthConfig = + realNameAuthProvider.config as TencentCloudFaceAuthConfig; + + if (!config) { + throw new Error('faceidRealNameAuth: Real name authentication configuration not found'); + } + + const realNameAuthReward = config.realNameAuthReward; + + const validationResult = schema.safeParse(req.body); + if (!validationResult.success) { + return jsonRes(res, { + code: 400, + message: 'Invalid request body', + data: validationResult.error.issues + }); + } + + const { transAmt } = validationResult.data; + + const info = await globalPrisma.enterpriseRealNameInfo.findUnique({ + where: { userUid: payload.userUid } + }); + + if (!info || !info.additionalInfo) { + return jsonRes(res, { + code: 404, + data: { + authState: 'failed' + }, + message: 'Enterprise auth info not found' + }); + } + + if (info.isVerified) { + return jsonRes(res, { + code: 400, + message: 'Enterprise real name authentication already verified' + }); + } + + const additionalInfo = info.additionalInfo as unknown as AdditionalInfo; + + if (additionalInfo.transAmt !== transAmt) { + return jsonRes(res, { + code: 200, + message: 'transAmt_not_match', + data: { + authState: 'failed' + } + }); + } + + if (realNameAuthReward) { + await globalPrisma.$transaction(async (globalPrisma) => { + const currentAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid } + }); + + if (!currentAccount.balance) { + throw new Error('enterpriseRealNameVerify: Account balance not found'); + } + + const currentActivityBonus = currentAccount.activityBonus || BigInt(0); + const realnameReward = BigInt(realNameAuthReward); + + const newActivityBonus = currentActivityBonus + realnameReward; + const newBalance = currentAccount.balance + realnameReward; + + const updatedAccount = await globalPrisma.account.update({ + where: { userUid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + const enterpriseRealNameInfo = await globalPrisma.enterpriseRealNameInfo.update({ + where: { userUid: payload.userUid }, + data: { + isVerified: true, + additionalInfo: { + ...additionalInfo, + paymentStatus: PAYMENTSTATUS.SUCCESS + } + } + }); + + const accountTransaction = await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_REWARD', + userUid: userUid, + balance: realnameReward, + balance_before: currentAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: currentAccount.deduction_balance, + message: 'Real name authentication reward', + billing_id: enterpriseRealNameInfo.id // You'll need to implement this function + } + }); + + await globalPrisma.userTask.updateMany({ + where: { + userUid, + task: { + taskType: 'REAL_NAME_AUTH' + }, + status: 'NOT_COMPLETED' + }, + data: { + rewardStatus: 'COMPLETED', + status: 'COMPLETED', + completedAt: new Date() + } + }); + + if (inviteInfo.inviterId && inviteInfo.amount) { + const inviterUser = await globalPrisma.user.findUniqueOrThrow({ + where: { id: inviteInfo.inviterId } + }); + + if (!inviterUser) { + throw new Error('enterpriseRealNameVerify: Inviter user not found'); + } + + const inviterAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid: inviterUser.uid } + }); + + if (!inviterAccount.balance) { + throw new Error('enterpriseRealNameVerify: Inviter account balance not found'); + } + + const currentActivityBonus = inviterAccount.activityBonus || BigInt(0); + const realnameInviteReward = inviteInfo.amount; + + const newActivityBonus = currentActivityBonus + realnameInviteReward; + const newBalance = inviterAccount.balance + realnameInviteReward; + + await globalPrisma.account.update({ + where: { userUid: inviterUser.uid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_INVITE_REWARD', + userUid: inviterUser.uid, + balance: realnameInviteReward, + balance_before: inviterAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: inviterAccount.deduction_balance, + message: 'Real name authentication invite reward', + billing_id: enterpriseRealNameInfo.id // You'll need to implement this function + } + }); + } + + return { + account: updatedAccount, + transaction: accountTransaction, + enterpriseRealNameInfo: enterpriseRealNameInfo + }; + }); + + return jsonRes(res, { + code: 200, + data: { + authState: 'success', + enterpriseRealName: additionalInfo.keyName + } + }); + } + + await globalPrisma.enterpriseRealNameInfo.update({ + where: { userUid: payload.userUid }, + data: { + isVerified: true, + additionalInfo: { + ...additionalInfo, + paymentStatus: PAYMENTSTATUS.SUCCESS + } + } + }); + + return jsonRes(res, { + code: 200, + data: { + authState: 'success', + enterpriseRealName: additionalInfo.keyName + } + }); + } catch (error) { + console.error('Error processing verify request:', error); + return jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} diff --git a/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts index a91e0aec8c95..3bee1fd161cc 100644 --- a/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts +++ b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts @@ -7,8 +7,9 @@ import { globalPrisma } from '@/services/backend/db/init'; import { GetDetectInfoEnhancedResponse } from 'tencentcloud-sdk-nodejs/tencentcloud/services/faceid/v20180301/faceid_models'; import { RealNameOSSConfigType } from '@/types'; import { Client, ClientOptions } from 'minio'; +import { getInviterInfo } from '@/utils/getInviteInfo'; -type TencentCloudFaceAuthConfig = { +export type TencentCloudFaceAuthConfig = { secretId: string; secretKey: string; ruleId: string; @@ -17,7 +18,7 @@ type TencentCloudFaceAuthConfig = { type JsonValue = string | number | boolean | object | null; -type RealNameAuthProvider = { +export type RealNameAuthProvider = { id: string; backend: string; authType: string; @@ -95,6 +96,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const isFaceRecognitionSuccess = userRealNameFaceAuthInfo.Text?.ErrCode === 0; const userUid = payload.userUid; + const userId = payload.userId; const timestamp = Date.now(); // Fetch existing user real name info @@ -106,6 +108,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return jsonRes(res, { code: 400, message: 'User real name info not found' }); } + if (userRealNameInfo.isVerified) { + return jsonRes(res, { code: 400, message: 'User real name info already verified' }); + } + + const inviteInfo = await getInviterInfo(userId); + let additionalInfo: AdditionalInfo = userRealNameInfo.additionalInfo; additionalInfo.faceRecognition.callback.isUsed = true; @@ -147,86 +155,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) additionalInfo.userMaterials.push(videoPath); } - if (isFaceRecognitionSuccess) { - if (realNameAuthReward) { - await globalPrisma.$transaction(async (globalPrisma) => { - const currentAccount = await globalPrisma.account.findUniqueOrThrow({ - where: { userUid } - }); - - if (!currentAccount.balance) { - throw new Error('faceidRealNameAuth: Account balance not found'); - } - - const currentActivityBonus = currentAccount.activityBonus || BigInt(0); - const rewardBigInt = BigInt(realNameAuthReward); - - const newActivityBonus = currentActivityBonus + rewardBigInt; - const newBalance = currentAccount.balance + rewardBigInt; - - const updatedAccount = await globalPrisma.account.update({ - where: { userUid }, - data: { - activityBonus: newActivityBonus, - balance: newBalance - } - }); - - const userRealNameInfo = await globalPrisma.userRealNameInfo.update({ - where: { userUid }, - data: { - realName: userRealNameFaceAuthInfo.Text?.Name, - idCard: userRealNameFaceAuthInfo.Text?.IdCard, - isVerified: true, - additionalInfo: additionalInfo - } - }); - - const accountTransaction = await globalPrisma.accountTransaction.create({ - data: { - type: 'REALNAME_AUTH_REWARD', - userUid: userUid, - balance: rewardBigInt, - balance_before: currentAccount.balance, - deduction_balance: 0, // No deduction in this case - deduction_balance_before: currentAccount.deduction_balance, - message: 'Real name authentication reward', - billing_id: userRealNameInfo.id // You'll need to implement this function - } - }); - - await globalPrisma.userTask.updateMany({ - where: { - userUid, - task: { - taskType: 'REAL_NAME_AUTH' - }, - status: 'NOT_COMPLETED' - }, - data: { - rewardStatus: 'COMPLETED', - status: 'COMPLETED', - completedAt: new Date() - } - }); - - return { - account: updatedAccount, - transaction: accountTransaction, - userRealNameInfo: userRealNameInfo - }; - }); - } else { - await globalPrisma.userRealNameInfo.update({ - where: { userUid }, - data: { - realName: userRealNameFaceAuthInfo.Text?.Name, - idCard: userRealNameFaceAuthInfo.Text?.IdCard, - isVerified: true, - additionalInfo: additionalInfo - } - }); - } + if (!isFaceRecognitionSuccess) { + await globalPrisma.userRealNameInfo.update({ + where: { userUid: userUid }, + data: { + isVerified: false, + idVerifyFailedTimes: { increment: 1 }, + additionalInfo: additionalInfo + } + }); res.setHeader('Content-Type', 'text/html'); return res.send(` @@ -246,17 +183,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } h1 { text-align: center; + color: #ff0000; /* Red color for error message */ } -

Real Name Authentication Successful

+

Real Name Authentication Failed

`); - } else { + } + + const realnameInfo = await globalPrisma.userRealNameInfo.findFirst({ + where: { + realName: userRealNameFaceAuthInfo.Text?.Name, + idCard: userRealNameFaceAuthInfo.Text?.IdCard, + isVerified: true + } + }); + + if (realnameInfo) { await globalPrisma.userRealNameInfo.update({ - where: { userUid }, + where: { userUid: userUid }, data: { isVerified: false, idVerifyFailedTimes: { increment: 1 }, @@ -287,11 +235,190 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) -

Real Name Authentication Failed

+

Real name information has been used

`); } + + if (realNameAuthReward) { + await globalPrisma.$transaction(async (globalPrisma) => { + const currentAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid } + }); + + if (!currentAccount.balance) { + throw new Error('faceidRealNameAuth: Account balance not found'); + } + + const currentActivityBonus = currentAccount.activityBonus || BigInt(0); + const realnameReward = BigInt(realNameAuthReward); + + const newActivityBonus = currentActivityBonus + realnameReward; + const newBalance = currentAccount.balance + realnameReward; + + const updatedAccount = await globalPrisma.account.update({ + where: { userUid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + const userRealNameInfo = await globalPrisma.userRealNameInfo.update({ + where: { userUid }, + data: { + realName: userRealNameFaceAuthInfo.Text?.Name, + idCard: userRealNameFaceAuthInfo.Text?.IdCard, + isVerified: true, + additionalInfo: additionalInfo + } + }); + + const accountTransaction = await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_REWARD', + userUid: userUid, + balance: realnameReward, + balance_before: currentAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: currentAccount.deduction_balance, + message: 'Real name authentication reward', + billing_id: userRealNameInfo.id // You'll need to implement this function + } + }); + + await globalPrisma.userTask.updateMany({ + where: { + userUid, + task: { + taskType: 'REAL_NAME_AUTH' + }, + status: 'NOT_COMPLETED' + }, + data: { + rewardStatus: 'COMPLETED', + status: 'COMPLETED', + completedAt: new Date() + } + }); + + if (inviteInfo.inviterId && inviteInfo.amount) { + const inviterUser = await globalPrisma.user.findUniqueOrThrow({ + where: { id: inviteInfo.inviterId } + }); + + if (!inviterUser) { + throw new Error('faceidRealNameAuth: Inviter user not found'); + } + + const inviterAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid: inviterUser.uid } + }); + + if (!inviterAccount.balance) { + throw new Error('faceidRealNameAuth: Inviter account balance not found'); + } + + const currentActivityBonus = inviterAccount.activityBonus || BigInt(0); + const realnameInviteReward = inviteInfo.amount; + + const newActivityBonus = currentActivityBonus + realnameInviteReward; + const newBalance = inviterAccount.balance + realnameInviteReward; + + await globalPrisma.account.update({ + where: { userUid: inviterUser.uid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_INVITE_REWARD', + userUid: inviterUser.uid, + balance: realnameInviteReward, + balance_before: inviterAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: inviterAccount.deduction_balance, + message: 'Real name authentication invite reward', + billing_id: userRealNameInfo.id // You'll need to implement this function + } + }); + } + + return { + account: updatedAccount, + transaction: accountTransaction, + userRealNameInfo: userRealNameInfo + }; + }); + + res.setHeader('Content-Type', 'text/html'); + return res.send(` + + + + + + Real Name Authentication + + + +

Real Name Authentication Successful

+ + + `); + } + + await globalPrisma.userRealNameInfo.update({ + where: { userUid }, + data: { + realName: userRealNameFaceAuthInfo.Text?.Name, + idCard: userRealNameFaceAuthInfo.Text?.IdCard, + isVerified: true, + additionalInfo: additionalInfo + } + }); + + res.setHeader('Content-Type', 'text/html'); + return res.send(` + + + + + + Real Name Authentication + + + +

Real Name Authentication Successful

+ + + `); } catch (error) { console.error('faceidRealNameAuth: Internal error'); if (error instanceof Error) { @@ -351,16 +478,16 @@ async function getUserRealNameInfo( credential: { secretId: config.secretId, secretKey: config.secretKey - }, - region: '', - profile: { - signMethod: 'HmacSHA256', - httpProfile: { - endpoint: 'faceid.tencentcloudapi.com', - reqMethod: 'POST', - reqTimeout: 30 // Request timeout, default 60s - } } + // region: '', + // profile: { + // signMethod: 'HmacSHA256', + // httpProfile: { + // endpoint: 'faceid.tencentcloudapi.com', + // reqMethod: 'POST', + // reqTimeout: 30 // Request timeout, default 60s + // } + // } }); const params = { diff --git a/frontend/desktop/src/pages/api/account/generateRealNameQRcodeUri.ts b/frontend/desktop/src/pages/api/account/generateRealNameQRcodeUri.ts index 3120f8a859ac..d184611722df 100644 --- a/frontend/desktop/src/pages/api/account/generateRealNameQRcodeUri.ts +++ b/frontend/desktop/src/pages/api/account/generateRealNameQRcodeUri.ts @@ -179,16 +179,16 @@ async function generateRealNameQRcodeUri( credential: { secretId: config.secretId, secretKey: config.secretKey - }, - region: '', - profile: { - signMethod: 'HmacSHA256', - httpProfile: { - endpoint: 'faceid.tencentcloudapi.com', - reqMethod: 'POST', - reqTimeout: 30 // Request timeout, default 60s - } } + // region: '', + // profile: { + // signMethod: 'HmacSHA256', + // httpProfile: { + // endpoint: 'faceid.tencentcloudapi.com', + // reqMethod: 'POST', + // reqTimeout: 30 // Request timeout, default 60s + // } + // } }); const params = { diff --git a/frontend/desktop/src/pages/api/auth/info.ts b/frontend/desktop/src/pages/api/auth/info.ts index cdb11082c915..1532dc4b419b 100644 --- a/frontend/desktop/src/pages/api/auth/info.ts +++ b/frontend/desktop/src/pages/api/auth/info.ts @@ -73,7 +73,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) id: string; name: string; realName?: string; - enterpriseVerificationStatus?: string; enterpriseRealName?: string; userRestrictedLevel?: number; } = { @@ -105,13 +104,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) })) }; - if (realNameInfo && realNameInfo.isVerified) { - info.realName = realNameInfo.realName || undefined; + if (realNameInfo && realNameInfo.isVerified && realNameInfo.realName) { + info.realName = realNameInfo.realName; } - if (enterpriseRealNameInfo) { - info.enterpriseVerificationStatus = enterpriseRealNameInfo.verificationStatus || undefined; - info.enterpriseRealName = enterpriseRealNameInfo.enterpriseName || undefined; + if ( + enterpriseRealNameInfo && + enterpriseRealNameInfo.isVerified && + enterpriseRealNameInfo.enterpriseName + ) { + info.enterpriseRealName = enterpriseRealNameInfo.enterpriseName; } if (restrictedUser) { diff --git a/frontend/desktop/src/pages/api/desktop/getResource.ts b/frontend/desktop/src/pages/api/desktop/getResource.ts index 8e569580b538..784281b59754 100644 --- a/frontend/desktop/src/pages/api/desktop/getResource.ts +++ b/frontend/desktop/src/pages/api/desktop/getResource.ts @@ -23,6 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) let totalMemoryLimits = 0; let totalStorageRequests = 0; let runningPodCount = 0; + let totalGpuCount = 0; let totalPodCount = 0; for (const pod of result.body.items) { @@ -40,9 +41,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const limits = container?.resources.limits as { cpu: string; memory: string; + ['nvidia.com/gpu']?: string; }; totalCpuLimits += parseResourceValue(limits.cpu); totalMemoryLimits += parseResourceValue(limits.memory); + + totalGpuCount += Number(limits['nvidia.com/gpu'] || 0); } if (!pod?.spec?.volumes) continue; @@ -65,7 +69,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) totalMemory: totalMemoryLimits.toFixed(2), totalStorage: totalStorageRequests.toFixed(2), runningPodCount, - totalPodCount + totalPodCount, + totalGpuCount // result: result.body.items } }); diff --git a/frontend/desktop/src/pages/api/platform/getAppConfig.ts b/frontend/desktop/src/pages/api/platform/getAppConfig.ts index 1c74133ba509..617959d8f2f1 100644 --- a/frontend/desktop/src/pages/api/platform/getAppConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getAppConfig.ts @@ -1,23 +1,24 @@ +import { getAuthClientConfig } from '@/pages/api/platform/getAuthConfig'; +import { getCloudConfig } from '@/pages/api/platform/getCloudConfig'; +import { getLayoutConfig } from '@/pages/api/platform/getLayoutConfig'; +import { + commitTransactionjob, + finishTransactionJob, + runTransactionjob +} from '@/services/backend/cronjob'; import { jsonRes } from '@/services/backend/response'; -import type { NextApiRequest, NextApiResponse } from 'next'; import { AppClientConfigType, AuthClientConfigType, CloudConfigType, CommonClientConfigType, DefaultAppClientConfig, - LayoutConfigType + LayoutConfigType, + TrackingConfigType } from '@/types/system'; -import { getCloudConfig } from '@/pages/api/platform/getCloudConfig'; -import { getAuthClientConfig } from '@/pages/api/platform/getAuthConfig'; -import { getLayoutConfig } from '@/pages/api/platform/getLayoutConfig'; -import { getCommonClientConfig } from './getCommonConfig'; import { Cron } from 'croner'; -import { - commitTransactionjob, - finishTransactionJob, - runTransactionjob -} from '@/services/backend/cronjob'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getCommonClientConfig } from './getCommonConfig'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const config = await getAppConfig(); @@ -31,7 +32,8 @@ function genResConfig( cloudConf: CloudConfigType, authConf: AuthClientConfigType, commonConf: CommonClientConfigType, - layoutConf: LayoutConfigType + layoutConf: LayoutConfigType, + tracking: Required ): AppClientConfigType { return { cloud: cloudConf, @@ -39,7 +41,8 @@ function genResConfig( desktop: { auth: authConf, layout: layoutConf - } + }, + tracking: tracking }; } @@ -49,7 +52,12 @@ export async function getAppConfig(): Promise { const authConf = await getAuthClientConfig(); const commonConf = await getCommonClientConfig(); const layoutConf = await getLayoutConfig(); - const conf = genResConfig(cloudConf, authConf, commonConf, layoutConf); + const _tracking = global.AppConfig.tracking; + const tracking: Required = { + websiteId: _tracking.websiteId || '', + hostUrl: _tracking.hostUrl || '' + }; + const conf = genResConfig(cloudConf, authConf, commonConf, layoutConf, tracking); if (!global.commitCroner) { // console.log('init commit croner'); global.commitCroner = new Cron('* * * * * *', commitTransactionjob, { diff --git a/frontend/desktop/src/pages/api/platform/getCommonConfig.ts b/frontend/desktop/src/pages/api/platform/getCommonConfig.ts index 87733b9937f0..c34c82b3308b 100644 --- a/frontend/desktop/src/pages/api/platform/getCommonConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getCommonConfig.ts @@ -6,8 +6,8 @@ import { DefaultCommonClientConfig } from '@/types/system'; import { readFileSync } from 'fs'; -import type { NextApiRequest, NextApiResponse } from 'next'; import yaml from 'js-yaml'; +import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const config = await getCommonClientConfig(); @@ -18,13 +18,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } function genResCommonClientConfig(common: CommonConfigType): CommonClientConfigType { return { + trackingEnabled: !!common.trackingEnabled, enterpriseRealNameAuthEnabled: !!common.enterpriseRealNameAuthEnabled, realNameAuthEnabled: !!common.realNameAuthEnabled, realNameReward: common.realNameReward || 0, guideEnabled: !!common.guideEnabled, rechargeEnabled: !!common.rechargeEnabled, - cfSiteKey: common.cfSiteKey || '', - enterpriseSupportingMaterials: common.enterpriseSupportingMaterials || '' + cfSiteKey: common.cfSiteKey || '' }; } export async function getCommonClientConfig(): Promise { diff --git a/frontend/desktop/src/pages/index.tsx b/frontend/desktop/src/pages/index.tsx index 6d74c5787c4c..8c375187ef7c 100644 --- a/frontend/desktop/src/pages/index.tsx +++ b/frontend/desktop/src/pages/index.tsx @@ -1,5 +1,6 @@ import { nsListRequest, switchRequest } from '@/api/namespace'; import DesktopContent from '@/components/desktop_content'; +import { trackEventName } from '@/constants/account'; import useAppStore from '@/stores/app'; import useCallbackStore from '@/stores/callback'; import { useConfigStore } from '@/stores/config'; @@ -14,6 +15,7 @@ import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; import { compareFirstLanguages } from '@/utils/tools'; import { Box, useColorMode } from '@chakra-ui/react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import dayjs from 'dayjs'; import { jwtDecode } from 'jwt-decode'; import { isString } from 'lodash'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; @@ -32,13 +34,13 @@ export const MoreAppsContext = createContext(null); export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: string }) { const router = useRouter(); - const { isUserLogin } = useSessionStore(); + const { firstUse, setFirstUse, isUserLogin } = useSessionStore(); const { colorMode, toggleColorMode } = useColorMode(); const init = useAppStore((state) => state.init); const setAutoLaunch = useAppStore((state) => state.setAutoLaunch); const { autolaunchWorkspaceUid } = useAppStore(); const { session } = useSessionStore(); - const { layoutConfig } = useConfigStore(); + const { layoutConfig, commonConfig, trackingConfig } = useConfigStore(); const { workspaceInviteCode, setWorkspaceInviteCode } = useCallbackStore(); const { setCanShowGuide } = useDesktopConfigStore(); @@ -78,6 +80,9 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str const is_login = isUserLogin(); const whitelistApps = ['system-template', 'system-fastdeploy']; if (!is_login) { + // clear firstusetime + setFirstUse(null); + const { appkey, appQuery } = parseOpenappQuery((query?.openapp as string) || ''); // Invited new user if (query?.uid && typeof query?.uid === 'string') { @@ -95,6 +100,7 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str if (isString(query?.workspaceUid)) workspaceUid = query.workspaceUid; if (appkey && typeof appQuery === 'string') setAutoLaunch(appkey, { raw: appQuery }, workspaceUid); + router.replace(destination); } else { let workspaceUid: string | undefined; @@ -205,7 +211,31 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str return; } }, [workspaceInviteCode]); - + useEffect(() => { + (async (state) => { + try { + if ( + commonConfig?.trackingEnabled && + (!firstUse || !dayjs(firstUse).isSame(dayjs(), 'day')) + ) { + const umami = window.umami; + if (!!umami) { + const result = await umami.track(trackEventName.dailyLoginFirst, { + userId: session?.user.userId!, + userUid: session?.user.userUid! + }); + if (result.ok && result.status === 200) { + setFirstUse(new Date()); + } else { + console.error('Failed to update first use date'); + } + } + } + } catch (e) { + console.log(e); + } + })(); + }, [commonConfig, firstUse]); return ( diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index 93790424131b..0ce532ce98fc 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -12,7 +12,8 @@ import { User, UserStatus } from 'prisma/global/generated/client'; -import { enableSignUp } from '../enable'; +import { enableSignUp, enableTracking } from '../enable'; +import { trackSignUp } from './tracking'; type TransactionClient = Omit< PrismaClient, @@ -254,7 +255,6 @@ export async function signUpByPassword({ return { user }; }); - return result; } catch (error) { console.error('globalAuth: Error during sign up:', error); @@ -329,13 +329,21 @@ export const getGlobalToken = async ({ password, semData }); - result && (user = result.user); - if (inviterId && result) { - inviteHandler({ - inviterId: inviterId, - inviteeId: result?.user.name, - signResult: result - }); + if (!!result) { + user = result.user; + if (inviterId && result) { + inviteHandler({ + inviterId: inviterId, + inviteeId: result?.user.name, + signResult: result + }); + } + if (enableTracking()) { + await trackSignUp({ + userId: result.user.id, + userUid: result.user.uid + }); + } } } else { const result = await signInByPassword({ @@ -356,22 +364,30 @@ export const getGlobalToken = async ({ avatar_url, semData }); - result && (user = result.user); - if (inviterId && result) { - inviteHandler({ - inviterId: inviterId, - inviteeId: result?.user.name, - signResult: result - }); - } - if (bdVid && result) { - uploadConvertData({ newType: [3], bdVid }) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); + if (result) { + user = result.user; + if (inviterId) { + inviteHandler({ + inviterId: inviterId, + inviteeId: result?.user.name, + signResult: result }); + } + if (bdVid) { + await uploadConvertData({ newType: [3], bdVid }) + .then((res) => { + console.log(res); + }) + .catch((err) => { + console.log(err); + }); + } + if (enableTracking()) { + await trackSignUp({ + userId: result.user.id, + userUid: result.user.uid + }); + } } } else { const result = await signIn({ diff --git a/frontend/desktop/src/services/backend/tracking/index.ts b/frontend/desktop/src/services/backend/tracking/index.ts new file mode 100644 index 000000000000..edb29b5d5f9f --- /dev/null +++ b/frontend/desktop/src/services/backend/tracking/index.ts @@ -0,0 +1,31 @@ +import { trackEventName } from '@/constants/account'; +import { Umami } from '@umami/node'; +const getUmami = () => { + return new Umami({ + websiteId: global.AppConfig.tracking.websiteId, + hostUrl: global.AppConfig.tracking.hostUrl + }); +}; +export type TLoginPayload = { + userUid: string; + userId: string; +}; +export const trackSignUp = (data: TLoginPayload) => { + const umami = getUmami(); + return umami + .track(trackEventName.signUp, data) + .then((res) => { + console.log('[tracking][signUp][success]'); + }) + .catch((e) => { + console.error('[tracking][signUp]:', e); + return Promise.resolve(null); + }); +}; +export const trackDailyLoginFirst = (data: TLoginPayload) => { + const umami = getUmami(); + return umami.track(trackEventName.dailyLoginFirst, data).catch((e) => { + console.error('[tracking][dailyLoginFirst]:', e); + return Promise.resolve(null); + }); +}; diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index 2a650da533ec..e08e93ee7c25 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -17,6 +17,7 @@ export const getBillingUrl = () => global.AppConfig.desktop.auth.billingUrl || ' export const getWorkorderUrl = () => global.AppConfig.desktop.auth.workorderUrl || ''; export const getCvmUrl = () => global.AppConfig.desktop.auth.cloudVitrualMachineUrl || ''; export const getTeamLimit = () => global.AppConfig.desktop.teamManagement?.maxTeamCount || 50; +export const enableTracking = () => !!global.AppConfig.common.trackingEnabled; export const getTeamInviteLimit = () => global.AppConfig.desktop.teamManagement?.maxTeamMemberCount || 50; diff --git a/frontend/desktop/src/stores/config.ts b/frontend/desktop/src/stores/config.ts index be7a147fba95..951a9653b604 100644 --- a/frontend/desktop/src/stores/config.ts +++ b/frontend/desktop/src/stores/config.ts @@ -1,19 +1,21 @@ +import { getAppConfig } from '@/api/platform'; import { + AuthClientConfigType, CloudConfigType, + CommonClientConfigType, LayoutConfigType, - AuthClientConfigType, - CommonClientConfigType + TrackingConfigType } from '@/types'; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; -import { getAppConfig } from '@/api/platform'; type State = { cloudConfig?: CloudConfigType; authConfig?: AuthClientConfigType; commonConfig?: CommonClientConfigType; layoutConfig?: LayoutConfigType; + trackingConfig?: TrackingConfigType; initAppConfig: () => Promise; }; @@ -24,11 +26,12 @@ export const useConfigStore = create()( authConfig: undefined, commonConfig: undefined, layoutConfig: undefined, - + trackingConfig: undefined, async initAppConfig() { const data = await getAppConfig(); console.log('initAppConfig', data.data); set((state) => { + state.trackingConfig = data.data.tracking; state.layoutConfig = data.data.desktop.layout; state.authConfig = data.data.desktop.auth; state.cloudConfig = data.data.cloud; diff --git a/frontend/desktop/src/stores/session.ts b/frontend/desktop/src/stores/session.ts index 051e7c50ad77..064e7127c0aa 100644 --- a/frontend/desktop/src/stores/session.ts +++ b/frontend/desktop/src/stores/session.ts @@ -13,9 +13,11 @@ type SessionState = { token: string; provider?: OauthProvider; oauth_state: string; + firstUse: Date | null; setSession: (ss: Session) => void; setSessionProp: (key: T, value: Session[T]) => void; delSession: () => void; + setFirstUse: (d: Date | null) => void; isUserLogin: () => boolean; /* when proxy oauth2.0 ,the domainState need to be used @@ -26,6 +28,7 @@ type SessionState = { action: string; statePayload: string[]; }; + setProvider: (provider?: OauthProvider) => void; setToken: (token: string) => void; lastWorkSpaceId: string; @@ -37,9 +40,15 @@ const useSessionStore = create()( immer((set, get) => ({ session: undefined, provider: undefined, + firstUse: null, oauth_state: '', token: '', lastWorkSpaceId: '', + setFirstUse(d) { + set({ + firstUse: d + }); + }, setSession: (ss: Session) => set({ session: ss }), setSessionProp: (key: keyof Session, value: any) => { set((state) => { diff --git a/frontend/desktop/src/types/index.ts b/frontend/desktop/src/types/index.ts index deff17a71ee2..5746e3739978 100644 --- a/frontend/desktop/src/types/index.ts +++ b/frontend/desktop/src/types/index.ts @@ -1,18 +1,19 @@ -import { type MongoClient } from 'mongodb'; -import { type AppConfigType } from './system'; +import { Umami } from '@umami/node'; import { Cron } from 'croner'; +import { type MongoClient } from 'mongodb'; import { Transporter } from 'nodemailer'; import SMTPPool from 'nodemailer/lib/smtp-pool'; +import { type AppConfigType } from './system'; export * from './api'; -export * from './session'; export * from './app'; export * from './crd'; +export * from './license'; +export * from './login'; export * from './payment'; +export * from './region'; +export * from './session'; export * from './system'; -export * from './login'; export * from './tools'; -export * from './license'; -export * from './region'; declare global { var mongodb: MongoClient | null; @@ -23,4 +24,5 @@ declare global { var WechatAccessToken: string | undefined; var WechatExpiresIn: number | undefined; var nodemailer: Transporter | undefined; + var umami: Umami | undefined; } diff --git a/frontend/desktop/src/types/response/enterpriseRealName.ts b/frontend/desktop/src/types/response/enterpriseRealName.ts new file mode 100644 index 000000000000..a480d2879af2 --- /dev/null +++ b/frontend/desktop/src/types/response/enterpriseRealName.ts @@ -0,0 +1,29 @@ +export enum PAYMENTSTATUS { + PROCESSING = 'PROCESSING', + FAILED = 'FAILED', + SUCCESS = 'SUCCESS', + CANCEL = 'USER_CANCEL' +} + +export interface EnterpriseAuthInfo { + paymentStatus: PAYMENTSTATUS; + key: string; + accountBank: string; + accountNo: string; + keyName: string; + usrName: string; + contactInfo: string; + remainingAttempts: number; +} + +export interface AdditionalInfo { + paymentStatus: PAYMENTSTATUS; + key: string; + accountBank: string; + accountNo: string; + keyName: string; + usrName: string; + contactInfo: string; + transAmt: string; + authTimes: number; +} diff --git a/frontend/desktop/src/types/session.ts b/frontend/desktop/src/types/session.ts index 043a45bae711..a8d762d594fa 100644 --- a/frontend/desktop/src/types/session.ts +++ b/frontend/desktop/src/types/session.ts @@ -8,7 +8,6 @@ export type OAuthToken = { export type UserInfo = { readonly userRestrictedLevel?: number; readonly realName?: string; - readonly enterpriseVerificationStatus?: string; readonly enterpriseRealName?: string; readonly k8s_username: string; readonly name: string; diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index 2704631a701c..2743e54b39a0 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -9,7 +9,6 @@ export type CloudConfigType = { export type CommonConfigType = { enterpriseRealNameAuthEnabled: boolean; - enterpriseSupportingMaterials: string; realNameAuthEnabled: boolean; realNameReward: number; guideEnabled: boolean; @@ -20,6 +19,7 @@ export type CommonConfigType = { objectstorageUrl: string; applaunchpadUrl: string; dbproviderUrl: string; + trackingEnabled: boolean; }; export type CommonClientConfigType = DeepRequired< Omit< @@ -64,7 +64,6 @@ export type LayoutConfigType = { customerServiceURL?: string; forcedLanguage?: string; currencySymbol?: 'shellCoin' | 'cny' | 'usd'; - protocol?: ProtocolConfigType; common: { githubStarEnabled: boolean; @@ -177,7 +176,19 @@ export type AuthClientConfigType = { 'cloudVitrualMachineUrl' ] > ->; +> & { + idp: { + sms: { + enabled: boolean; + ali: { + enabled: boolean; + }; + email: { + enabled: boolean; + }; + }; + }; +}; export type JwtConfigType = { internal?: string; @@ -194,6 +205,11 @@ export type DesktopConfigType = { }; }; +export type TrackingConfigType = { + websiteId?: string; + hostUrl?: string; +}; + export type RealNameOSSConfigType = { accessKey: string; accessKeySecret: string; @@ -209,18 +225,20 @@ export type AppConfigType = { common: CommonConfigType; database: DatabaseConfigType; desktop: DesktopConfigType; + tracking: TrackingConfigType; realNameOSS: RealNameOSSConfigType; }; export type AppClientConfigType = { cloud: CloudConfigType; common: CommonClientConfigType; + tracking: Required; desktop: DesktopConfigType; }; export const DefaultCommonClientConfig: CommonClientConfigType = { enterpriseRealNameAuthEnabled: false, - enterpriseSupportingMaterials: '', + trackingEnabled: false, realNameAuthEnabled: false, realNameReward: 0, guideEnabled: false, @@ -309,10 +327,13 @@ export const DefaultAuthClientConfig: AuthClientConfigType = { }, billingToken: '' }; - export const DefaultAppClientConfig: AppClientConfigType = { cloud: DefaultCloudConfig, common: DefaultCommonClientConfig, + tracking: { + websiteId: '', + hostUrl: '' + }, desktop: { layout: DefaultLayoutConfig, auth: DefaultAuthClientConfig diff --git a/frontend/desktop/src/types/token.ts b/frontend/desktop/src/types/token.ts index f2852f59b7eb..9486c57b6e43 100644 --- a/frontend/desktop/src/types/token.ts +++ b/frontend/desktop/src/types/token.ts @@ -1,6 +1,7 @@ export type AuthenticationTokenPayload = { userUid: string; userId: string; + regionUid?: string; }; export type AccessTokenPayload = { regionUid: string; diff --git a/frontend/desktop/src/utils/getInviteInfo.ts b/frontend/desktop/src/utils/getInviteInfo.ts new file mode 100644 index 000000000000..9b2e4b88fa7c --- /dev/null +++ b/frontend/desktop/src/utils/getInviteInfo.ts @@ -0,0 +1,51 @@ +import { AuthConfigType } from '@/types/system'; +interface InviterResponse { + code: number; + message: string; + data?: { + inviterId: string; + reward: { + amount: number; + }; + }; +} + +export async function getInviterInfo( + inviteeId: string +): Promise<{ inviterId?: string; amount?: bigint }> { + const conf = global.AppConfig?.desktop.auth as AuthConfigType; + const inviteEnabled = conf.invite?.enabled || false; + const secretKey = conf.invite?.lafSecretKey || ''; + const baseUrl = conf.invite?.lafBaseURL || ''; + + if (!inviteEnabled || !secretKey || !baseUrl) { + return {}; + } + + try { + const response = await fetch(`https://${baseUrl}/getInviter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + inviteeId, + secretKey + }) + }); + + const result: InviterResponse = await response.json(); + + if (result.code !== 200) { + return {}; + } + + return { + inviterId: result.data?.inviterId, + amount: result.data?.reward?.amount ? BigInt(result.data.reward.amount * 1000000) : BigInt(0) + }; + } catch (error) { + console.error('getInviteInfo error:', error); + return {}; + } +} diff --git a/frontend/desktop/src/utils/sessionConfig.ts b/frontend/desktop/src/utils/sessionConfig.ts index c1c44563f6b1..a594900ef845 100644 --- a/frontend/desktop/src/utils/sessionConfig.ts +++ b/frontend/desktop/src/utils/sessionConfig.ts @@ -22,7 +22,6 @@ export const sessionConfig = async ({ user: { userRestrictedLevel: infoData.data?.info.userRestrictedLevel || undefined, realName: infoData.data?.info.realName || undefined, - enterpriseVerificationStatus: infoData.data?.info.enterpriseVerificationStatus || undefined, enterpriseRealName: infoData.data?.info.enterpriseRealName || undefined, k8s_username: payload.userCrName, name: infoData.data?.info.nickname || '', diff --git a/frontend/packages/ui/src/components/Select/index.tsx b/frontend/packages/ui/src/components/Select/index.tsx index f75268a28e99..8958a5ab0985 100644 --- a/frontend/packages/ui/src/components/Select/index.tsx +++ b/frontend/packages/ui/src/components/Select/index.tsx @@ -54,7 +54,16 @@ const MySelect = ( } }); - const activeMenu = useMemo(() => list.find((item) => item.value === value), [list, value]); + const activeMenu = useMemo(() => { + const foundItem = list.find((item) => item.value === value); + if (!foundItem && value) { + return { + label: value, + value: value + }; + } + return foundItem; + }, [list, value]); return ( diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6e48aa7038fd..5320c887cae6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: '@tanstack/react-query': specifier: ^4.35.3 version: 4.36.1(react-dom@18.2.0)(react@18.2.0) + '@umami/node': + specifier: ^0.4.0 + version: 0.4.0 axios: specifier: ^1.5.1 version: 1.6.2 @@ -135,9 +138,6 @@ importers: eslint-config-next: specifier: 13.3.0 version: 13.3.0(eslint@8.38.0)(typescript@5.2.2) - formidable: - specifier: ^3.5.1 - version: 3.5.1 framer-motion: specifier: ^10.16.4 version: 10.16.5(react-dom@18.2.0)(react@18.2.0) @@ -247,9 +247,6 @@ importers: '@testing-library/react': specifier: ^14.0.0 version: 14.1.2(react-dom@18.2.0)(react@18.2.0) - '@types/formidable': - specifier: ^3.4.5 - version: 3.4.5 '@types/jest': specifier: ^29.5.10 version: 29.5.10 @@ -283,6 +280,9 @@ importers: '@types/react-dom': specifier: 18.0.11 version: 18.0.11 + '@types/umami-browser': + specifier: ^2.3.2 + version: 2.3.2 '@types/uuid': specifier: ^9.0.4 version: 9.0.7 @@ -482,6 +482,9 @@ importers: providers/aiproxy: dependencies: + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.48.2) '@sealos/ui': specifier: workspace:* version: link:../../packages/ui @@ -500,6 +503,12 @@ importers: date-fns: specifier: ^2.30.0 version: 2.30.0 + downshift: + specifier: ^9.0.8 + version: 9.0.8(react@18.2.0) + echarts: + specifier: ^5.4.3 + version: 5.4.3 i18next: specifier: ^23.11.5 version: 23.12.1 @@ -520,7 +529,7 @@ importers: version: 9.0.2 next: specifier: 14.2.5 - version: 14.2.5(@babel/core@7.23.5)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) + version: 14.2.5(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18 version: 18.2.0 @@ -533,9 +542,15 @@ importers: react-hook-form: specifier: ^7.46.2 version: 7.48.2(react@18.2.0) + react-json-view: + specifier: ^1.21.3 + version: 1.21.3(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) sealos-desktop-sdk: specifier: workspace:* version: link:../../packages/client-sdk + zod: + specifier: ^3.23.8 + version: 3.23.8 zustand: specifier: ^4.5.4 version: 4.5.4(@types/react@18.2.37)(immer@10.1.1)(react@18.2.0) @@ -612,6 +627,9 @@ importers: '@tanstack/react-query': specifier: ^4.35.3 version: 4.36.1(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-table': + specifier: ^8.10.7 + version: 8.10.7(react-dom@18.2.0)(react@18.2.0) ansi_up: specifier: ^5.2.1 version: 5.2.1 @@ -621,6 +639,9 @@ importers: base64-stream: specifier: ^1.0.0 version: 1.0.0 + date-fns: + specifier: ^2.30.0 + version: 2.30.0 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -678,6 +699,9 @@ importers: react: specifier: 18.2.0 version: 18.2.0 + react-day-picker: + specifier: ^8.8.2 + version: 8.9.1(date-fns@2.30.0)(react@18.2.0) react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) @@ -1475,6 +1499,9 @@ importers: '@prisma/client': specifier: ^5.10.2 version: 5.10.2(prisma@5.10.2) + '@sealos/driver': + specifier: workspace:^ + version: link:../../packages/driver '@sealos/ui': specifier: workspace:^ version: link:../../packages/ui @@ -5526,6 +5553,13 @@ packages: dependencies: regenerator-runtime: 0.14.0 + /@babel/runtime@7.26.0: + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: false + /@babel/template@7.22.15: resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} engines: {node: '>=6.9.0'} @@ -10513,6 +10547,10 @@ packages: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false + /@types/umami-browser@2.3.2: + resolution: {integrity: sha512-Y/6dm2EDSw57x3nxFz0X4WoK0ykeWKEGJnrKVGSXE6r6LlibPcKbVr5jPcmEfwya9l2PWzzIlOXRVH/7RU/dUQ==} + dev: true + /@types/unist@2.0.10: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: false @@ -10869,6 +10907,10 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@umami/node@0.4.0: + resolution: {integrity: sha512-pyphprbiF7KiDSc+SWZ4/rVM8B5vU27zIiFfEPj2lEqczpI4xAKSp+dM3tlzyRAWJL32fcbCfAaLGhJZQV13Rg==} + dev: false + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -11776,6 +11818,10 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base16@1.0.0: + resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==} + dev: false + /base64-arraybuffer@0.1.2: resolution: {integrity: sha512-ewBKKVVPIl78B26mYQHYlaxR7NydMiD/GxwLNIwTAfLIE4xhN2Gxcy30//azq5UrejXjzGpWjcBu3NUJxzMMzg==} engines: {node: '>= 0.6.0'} @@ -12422,6 +12468,10 @@ packages: resolution: {integrity: sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==} dev: false + /compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -12587,6 +12637,14 @@ packages: hasBin: true dev: false + /cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /cross-fetch@4.0.0: resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} dependencies: @@ -13273,6 +13331,19 @@ packages: minimatch: 3.1.2 dev: false + /downshift@9.0.8(react@18.2.0): + resolution: {integrity: sha512-59BWD7+hSUQIM1DeNPLirNNnZIO9qMdIK5GQ/Uo8q34gT4B78RBlb9dhzgnh0HfQTJj4T/JKYD8KoLAlMWnTsA==} + peerDependencies: + react: '>=16.12.0' + dependencies: + '@babel/runtime': 7.26.0 + compute-scroll-into-view: 3.1.0 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + tslib: 2.6.2 + dev: false + /drange@1.1.1: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} @@ -14299,7 +14370,7 @@ packages: debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color dev: true @@ -15436,6 +15507,32 @@ packages: bser: 2.1.1 dev: true + /fbemitter@3.0.0: + resolution: {integrity: sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==} + dependencies: + fbjs: 3.0.5 + transitivePeerDependencies: + - encoding + dev: false + + /fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + dev: false + + /fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + dependencies: + cross-fetch: 3.1.8 + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.39 + transitivePeerDependencies: + - encoding + dev: false + /fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} dev: false @@ -15538,6 +15635,18 @@ packages: /flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + /flux@4.0.4(react@18.2.0): + resolution: {integrity: sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==} + peerDependencies: + react: ^15.0.2 || ^16.0.0 || ^17.0.0 + dependencies: + fbemitter: 3.0.0 + fbjs: 3.0.5 + react: 18.2.0 + transitivePeerDependencies: + - encoding + dev: false + /fmin@0.0.2: resolution: {integrity: sha512-sSi6DzInhl9d8yqssDfGZejChO8d2bAGIpysPsvYsxFe898z89XhCZg6CPNV3nhUhFefeC/AXZK2bAJxlBjN6A==} dependencies: @@ -17999,9 +18108,17 @@ packages: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: false + /lodash.curry@4.1.1: + resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} + dev: false + /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + /lodash.flow@3.5.0: + resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} + dev: false + /lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false @@ -18096,6 +18213,7 @@ packages: /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + requiresBuild: true dependencies: yallist: 4.0.0 @@ -19515,6 +19633,48 @@ packages: - babel-plugin-macros dev: false + /next@14.2.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.2.5 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001594 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.5 + '@next/swc-darwin-x64': 14.2.5 + '@next/swc-linux-arm64-gnu': 14.2.5 + '@next/swc-linux-arm64-musl': 14.2.5 + '@next/swc-linux-x64-gnu': 14.2.5 + '@next/swc-linux-x64-musl': 14.2.5 + '@next/swc-win32-arm64-msvc': 14.2.5 + '@next/swc-win32-ia32-msvc': 14.2.5 + '@next/swc-win32-x64-msvc': 14.2.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /node-abi@3.54.0: resolution: {integrity: sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==} engines: {node: '>=10'} @@ -20252,6 +20412,12 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: false + /promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + dependencies: + asap: 2.0.6 + dev: false + /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -20306,6 +20472,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + /pure-color@1.3.0: + resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==} + dev: false + /pure-rand@6.0.4: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} dev: true @@ -20971,6 +21141,15 @@ packages: - prop-types dev: false + /react-base16-styling@0.6.0: + resolution: {integrity: sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==} + dependencies: + base16: 1.0.0 + lodash.curry: 4.1.1 + lodash.flow: 3.5.0 + pure-color: 1.3.0 + dev: false + /react-clientside-effect@1.2.6(react@18.2.0): resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: @@ -21145,6 +21324,23 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + /react-json-view@1.21.3(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==} + peerDependencies: + react: ^17.0.0 || ^16.3.0 || ^15.5.4 + react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 + dependencies: + flux: 4.0.4(react@18.2.0) + react: 18.2.0 + react-base16-styling: 0.6.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + react-textarea-autosize: 8.5.6(@types/react@18.2.37)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - encoding + dev: false + /react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false @@ -21262,6 +21458,20 @@ packages: refractor: 3.6.0 dev: false + /react-textarea-autosize@8.5.6(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + '@babel/runtime': 7.26.0 + react: 18.2.0 + use-composed-ref: 1.4.0(@types/react@18.2.37)(react@18.2.0) + use-latest: 1.3.0(@types/react@18.2.37)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /react-universal-interface@0.6.2(react@18.2.0)(tslib@2.6.2): resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} peerDependencies: @@ -22683,6 +22893,23 @@ packages: react: 18.2.0 dev: false + /styled-jsx@5.1.1(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.2.0 + dev: false + /stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false @@ -23379,6 +23606,11 @@ packages: resolution: {integrity: sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==} dev: false + /ua-parser-js@1.0.39: + resolution: {integrity: sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==} + hasBin: true + dev: false + /uglify-js@2.8.29: resolution: {integrity: sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==} engines: {node: '>=0.8.0'} @@ -23593,6 +23825,19 @@ packages: tslib: 2.6.2 dev: false + /use-composed-ref@1.4.0(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.37 + react: 18.2.0 + dev: false + /use-intl@3.17.2(react@18.2.0): resolution: {integrity: sha512-9lPgt41nS8x4AYCLfIC9VKCmamnVxzPM2nze7lpp/I1uaSSQvIz5MQpYUFikv08cMUsCwAWahU0e+arHInpdcw==} peerDependencies: @@ -23603,6 +23848,33 @@ packages: react: 18.2.0 dev: false + /use-isomorphic-layout-effect@1.2.0(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.37 + react: 18.2.0 + dev: false + + /use-latest@1.3.0(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.37 + react: 18.2.0 + use-isomorphic-layout-effect: 1.2.0(@types/react@18.2.37)(react@18.2.0) + dev: false + /use-sidecar@1.1.2(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index 2f83f5ecd23c..a64ba9064fb1 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -1,25 +1,124 @@ -import { KeysSearchResponse } from '@/app/api/get-keys/route' -import { QueryParams, SearchResponse } from '@/app/api/get-logs/route' -import { QueryParams as KeysQueryParams } from '@/app/api/get-keys/route' -import { GET, POST, DELETE } from '@/utils/request' -import { ModelPrice } from '@/types/backend' +import { GET, POST, DELETE, PUT } from '@/utils/frontend/request' +import { ChannelQueryParams, GetChannelsResponse } from '@/app/api/admin/channel/route' +import { ChannelStatus, CreateChannelRequest } from '@/types/admin/channels/channelInfo' +import { ApiResp } from '@/types/api' +import { GetOptionResponse } from '@/app/api/admin/option/route' +import { BatchOptionData } from '@/types/admin/option' +import { GetEnabledModelsResponse } from '@/app/api/models/enabled/route' +import { GetTokensQueryParams, GetTokensResponse } from '@/app/api/user/token/route' +import { TokenInfo } from '@/types/user/token' +import { UserLogSearchResponse } from '@/app/api/user/log/route' +import { UserLogQueryParams } from '@/app/api/user/log/route' +import { GlobalLogQueryParams, GlobalLogSearchResponse } from '@/app/api/admin/log/route' +import { GetAllChannelEnabledModelsResponse } from '@/app/api/models/builtin/channel/route' +import { GetDefaultModelAndModeMappingResponse } from '@/app/api/models/default/route' +import { GetChannelTypeNamesResponse } from '@/app/api/admin/channel/type-name/route' +import { GroupQueryParams, GroupStatus } from '@/types/admin/group' +import { GroupSearchResponse } from '@/app/api/admin/group/route' +import { GetAllChannelResponse } from '@/app/api/admin/channel/all/route' +import { DashboardQueryParams } from '@/app/api/user/dashboard/route' +import { DashboardResponse } from '@/types/user/dashboard' +import { UserLogDetailResponse } from '@/app/api/user/log/detail/[log_id]/route' export const initAppConfig = () => - GET<{ aiproxyBackend: string; currencySymbol: 'shellCoin' | 'cny' | 'usd' }>( - '/api/init-app-config' - ) + GET<{ + aiproxyBackend: string + currencySymbol: 'shellCoin' | 'cny' | 'usd' + docUrl: string + isInvitationActive: boolean + invitationUrl: string + }>('/api/init-app-config') -export const getModels = () => GET('/api/get-models') +// +export const getEnabledMode = () => GET('/api/models/enabled') -export const getModelPrices = () => GET('/api/get-mode-price') +// log +export const getUserLogs = (params: UserLogQueryParams) => + GET('/api/user/log', params) -export const getLogs = (params: QueryParams) => GET('/api/get-logs', params) +export const getUserLogDetail = (log_id: number) => + GET(`/api/user/log/detail/${log_id}`) -export const getKeys = (params: KeysQueryParams) => - GET('/api/get-keys', params) +// token +export const getTokens = (params: GetTokensQueryParams) => + GET('/api/user/token', params) -export const createKey = (name: string) => POST('/api/create-key', { name }) +export const createToken = (name: string) => + POST['data']>('/api/user/token', { name }) -export const deleteKey = (id: number) => DELETE(`/api/delete-key/${id}`) +export const deleteToken = (id: number) => DELETE(`/api/user/token/${id}`) -export const updateKey = (id: number, status: number) => POST(`/api/update-key/${id}`, { status }) +export const updateToken = (id: number, status: number) => + POST(`/api/user/token/${id}`, { status: status }) + +// dashboard +export const getDashboardData = (params: DashboardQueryParams) => + GET('/api/user/dashboard', params) +// ------------------------------------------------------------ +// + +export const getChannels = (params: ChannelQueryParams) => + GET('/api/admin/channel', params) + +// channel +export const createChannel = (params: CreateChannelRequest) => + POST('/api/admin/channel', params) + +export const updateChannel = (params: CreateChannelRequest, id: string) => + PUT(`/api/admin/channel/${id}`, params) + +export const updateChannelStatus = (id: string, status: ChannelStatus) => + POST(`/api/admin/channel/${id}/status`, { status }) + +export const getChannelTypeNames = () => + GET('/api/admin/channel/type-name') + +export const getAllChannels = () => GET('/api/admin/channel/all') + +export const deleteChannel = (id: string) => DELETE(`/api/admin/channel/${id}`) + +export const uploadChannels = (formData: FormData) => + POST('/api/admin/channel/upload', formData, { + headers: { + // Don't set Content-Type header here, it will be automatically set with the correct boundary + } + }) + +// channel built-in support models and default model default mode mapping +export const getChannelBuiltInSupportModels = () => + GET('/api/models/builtin/channel') + +export const getChannelDefaultModelAndDefaultModeMapping = () => + GET('/api/models/default') + +// option +export const getOption = () => GET('/api/admin/option') + +export const updateOption = (params: { key: string; value: string }) => + PUT(`/api/admin/option/`, params) + +export const batchOption = (params: BatchOptionData) => + PUT(`/api/admin/option/batch`, params) + +export const uploadOptions = (formData: FormData) => + POST('/api/admin/option/upload', formData, { + headers: { + // Don't set Content-Type header here, it will be automatically set with the correct boundary + } + }) + +// log +export const getGlobalLogs = (params: GlobalLogQueryParams) => + GET('/api/admin/log', params) + +// group +export const getGroups = (params: GroupQueryParams) => + GET('/api/admin/group', params) + +export const updateGroupStatus = (id: string, status: GroupStatus) => + POST(`/api/admin/group/${id}/status`, { status }) + +export const updateGroupQpm = (id: string, qpm: number) => + POST(`/api/admin/group/${id}/qpm`, { qpm }) + +export const deleteGroup = (id: string) => DELETE(`/api/admin/group/${id}`) diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/components/ChannelTable.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/components/ChannelTable.tsx new file mode 100644 index 000000000000..db14c632bb82 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/components/ChannelTable.tsx @@ -0,0 +1,750 @@ +'use client' +import { + Checkbox, + Box, + Flex, + Table, + TableContainer, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + Menu, + MenuButton, + MenuList, + MenuItem, + useDisclosure, + Spinner +} from '@chakra-ui/react' +import { + Column, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable +} from '@tanstack/react-table' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { ChannelInfo, ChannelStatus, ChannelType } from '@/types/admin/channels/channelInfo' +import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query' +import { useState, useMemo, useEffect } from 'react' +import { + deleteChannel, + getChannels, + getChannelTypeNames, + updateChannelStatus +} from '@/api/platform' +import SwitchPage from '@/components/common/SwitchPage' +import UpdateChannelModal from './UpdateChannelModal' +import { useMessage } from '@sealos/ui' +import { QueryKey } from '@/types/query-key' +import { downloadJson } from '@/utils/common' + +export default function ChannelTable({ + exportData +}: { + exportData: (data: ChannelInfo[]) => void +}) { + const { isOpen, onOpen, onClose } = useDisclosure() + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const [operationType, setOperationType] = useState<'create' | 'update'>('update') + const [channelInfo, setChannelInfo] = useState(undefined) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + + const [selectedChannels, setSelectedChannels] = useState([]) + + useEffect(() => { + exportData(selectedChannels) + }, [selectedChannels]) + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const queryClient = useQueryClient() + + const { isLoading: isChannelTypeNamesLoading, data: channelTypeNames } = useQuery({ + queryKey: [QueryKey.GetChannelTypeNames], + queryFn: () => getChannelTypeNames() + }) + + const { data, isLoading: isChannelsLoading } = useQuery({ + queryKey: [QueryKey.GetChannels, page, pageSize], + queryFn: () => getChannels({ page, perPage: pageSize }), + refetchOnReconnect: true, + onSuccess(data) { + setTotal(data?.total || 0) + } + }) + + const updateChannelStatusMutation = useMutation( + ({ id, status }: { id: string; status: number }) => updateChannelStatus(id, status), + { + onSuccess() { + message({ + status: 'success', + title: t('channel.updateSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + queryClient.invalidateQueries([QueryKey.GetChannels]) + queryClient.invalidateQueries([QueryKey.GetChannelTypeNames]) + }, + onError(err: any) { + message({ + status: 'warning', + title: t('channel.updateFailed'), + description: err?.message || t('channel.updateFailed'), + isClosable: true, + position: 'top' + }) + } + } + ) + const deleteChannelMutation = useMutation(({ id }: { id: string }) => deleteChannel(id), { + onSuccess() { + message({ + status: 'success', + title: t('channel.deleteSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + queryClient.invalidateQueries([QueryKey.GetChannels]) + queryClient.invalidateQueries([QueryKey.GetChannelTypeNames]) + }, + onError(err: any) { + message({ + status: 'warning', + title: t('channel.deleteFailed'), + description: err?.message || t('channel.deleteFailed'), + isClosable: true, + position: 'top' + }) + } + }) + + // Update the button click handlers in the table actions column: + const handleStatusUpdate = (id: string, currentStatus: number) => { + const newStatus = + currentStatus === ChannelStatus.ChannelStatusDisabled + ? ChannelStatus.ChannelStatusEnabled + : ChannelStatus.ChannelStatusDisabled + updateChannelStatusMutation.mutate({ id, status: newStatus }) + } + + const columnHelper = createColumnHelper() + + const handleHeaderCheckboxChange = (isChecked: boolean) => { + if (isChecked) { + setSelectedChannels(data?.channels || []) + } else { + setSelectedChannels([]) + } + } + + const handleRowCheckboxChange = (channel: ChannelInfo, isChecked: boolean) => { + if (isChecked) { + setSelectedChannels([...selectedChannels, channel]) + } else { + setSelectedChannels(selectedChannels.filter((c) => c.id !== channel.id)) + } + } + + const handleExportRow = (channel: ChannelInfo) => { + const channels = [channel] + const filename = `channel_${channels[0].id}_${new Date().toISOString()}.json` + downloadJson(channels, filename) + } + + const columns = [ + columnHelper.accessor((row) => row.id, { + id: 'id', + header: () => ( + + 0 && + selectedChannels.length === data.channels.length + } + onChange={(e) => handleHeaderCheckboxChange(e.target.checked)} + sx={{ + '.chakra-checkbox__control': { + width: '16px', + height: '16px', + border: '1px solid', + borderColor: 'grayModern.300', + background: 'grayModern.100', + transition: 'all 0.2s ease', + _checked: { + background: 'grayModern.500', + borderColor: 'grayModern.500' + } + } + }} + /> + + {t('channels.id')} + + + ), + cell: (info) => ( + + c.id === info.getValue())} + onChange={(e) => handleRowCheckboxChange(info.row.original, e.target.checked)} + sx={{ + '.chakra-checkbox__control': { + width: '16px', + height: '16px', + border: '1px solid', + borderColor: 'grayModern.300', + background: 'white', + _checked: { + background: 'grayModern.500', + borderColor: 'grayModern.500' + } + } + }} + /> + + {info.getValue()} + + + ) + }), + columnHelper.accessor((row) => row.name, { + id: 'name', + header: () => ( + + {t('channels.name')} + + ), + cell: (info) => ( + + {info.getValue()} + + ) + }), + columnHelper.accessor((row) => row.type, { + id: 'type', + header: () => ( + + {t('channels.type')} + + ), + cell: (info) => ( + + {channelTypeNames?.[String(info.getValue()) as ChannelType]} + + ) + }), + columnHelper.accessor((row) => row.request_count, { + id: 'request_count', + header: () => ( + + {t('channels.requestCount')} + + ), + cell: (info) => ( + + {info.getValue()} + + ) + }), + columnHelper.accessor((row) => row.status, { + id: 'status', + header: () => ( + + {t('channels.status')} + + ), + cell: (info) => { + const status = info.getValue() + let statusText = '' + let statusColor = '' + + switch (status) { + case ChannelStatus.ChannelStatusEnabled: + statusText = t('keystatus.enabled') + statusColor = 'green.600' + break + case ChannelStatus.ChannelStatusDisabled: + statusText = t('keystatus.disabled') + statusColor = 'red.600' + break + case ChannelStatus.ChannelStatusAutoDisabled: + statusText = t('channelStatus.autoDisabled') + statusColor = 'orange.500' + break + default: + statusText = t('keystatus.unknown') + statusColor = 'gray.500' + } + + return ( + + {statusText} + + ) + } + }), + + columnHelper.display({ + id: 'actions', + header: () => ( + + {t('channels.action')} + + ), + cell: (info) => ( + + + + + + + + {/* console.log('Export', info.row.original.id)}> + + + + + + + + + {t('channels.test')} + + */} + + handleStatusUpdate(String(info.row.original.id), info.row.original.status) + }> + {info.row.original.status === ChannelStatus.ChannelStatusEnabled ? ( + <> + + + + + {t('channels.disable')} + + + ) : ( + <> + + + + + {t('channels.enable')} + + + )} + + { + setOperationType('update') + setChannelInfo(info.row.original) + onOpen() + }}> + + + + + + + {t('channels.edit')} + + + handleExportRow(info.row.original)}> + + + + + {t('channels.export')} + + + + deleteChannelMutation.mutate({ id: String(info.row.original.id) })}> + + + + + {t('channels.delete')} + + + + + ) + }) + ] + + const tableData = useMemo(() => data?.channels || [], [data]) + + const table = useReactTable({ + data: tableData, + columns, + getCoreRowModel: getCoreRowModel() + }) + + return ( + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, i) => ( + + ))} + + ))} + + + {isChannelTypeNamesLoading || isChannelsLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + )} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ setPage(idx)} + /> + +
+ ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/components/UpdateChannelModal.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/components/UpdateChannelModal.tsx new file mode 100644 index 000000000000..9ad31ddf8ee3 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/components/UpdateChannelModal.tsx @@ -0,0 +1,833 @@ +'use client' +import { + Button, + Flex, + Text, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + FormControl, + Input, + FormErrorMessage, + ModalFooter, + FormLabel, + VStack, + Center, + Spinner, + Badge +} from '@chakra-ui/react' +import { useMessage } from '@sealos/ui' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { ChannelInfo, ChannelStatus, ChannelType } from '@/types/admin/channels/channelInfo' +import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query' +import { Dispatch, SetStateAction, useEffect } from 'react' +import { + getChannelBuiltInSupportModels, + getChannelDefaultModelAndDefaultModeMapping, + getChannelTypeNames +} from '@/api/platform' +import { FieldErrors, useForm, Controller } from 'react-hook-form' +import { z } from 'zod' +import { zodResolver } from '@hookform/resolvers/zod' +import { MultiSelectCombobox } from '@/components/common/MultiSelectCombobox' +import { SingleSelectCombobox } from '@/components/common/SingleSelectCombobox' +import ConstructModeMappingComponent from '@/components/common/ConstructModeMappingComponent' +import { createChannel, updateChannel } from '@/api/platform' +import { QueryKey } from '@/types/query-key' + +type Model = { + name: string + isDefault: boolean +} + +export const UpdateChannelModal = function ({ + isOpen, + onClose, + operationType, + channelInfo +}: { + isOpen: boolean + onClose: () => void + operationType: 'create' | 'update' + channelInfo?: ChannelInfo +}): JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const queryClient = useQueryClient() + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const { isLoading: isChannelTypeNamesLoading, data: channelTypeNames } = useQuery({ + queryKey: [QueryKey.GetChannelTypeNames], + queryFn: () => getChannelTypeNames() + }) + + const { isLoading: isBuiltInSupportModelsLoading, data: builtInSupportModels } = useQuery({ + queryKey: [QueryKey.GetAllChannelModes], + queryFn: () => getChannelBuiltInSupportModels() + }) + + const { isLoading: isDefaultEnabledModelsLoading, data: defaultEnabledModels } = useQuery({ + queryKey: [QueryKey.GetDefaultModelAndModeMapping], + queryFn: () => getChannelDefaultModelAndDefaultModeMapping() + }) + + // model type select combobox + const handleModelTypeDropdownItemFilter = (dropdownItems: string[], inputValue: string) => { + const lowerCasedInput = inputValue.toLowerCase() + return dropdownItems.filter( + (item) => !inputValue || item.toLowerCase().includes(lowerCasedInput) + ) + } + + const handleModelTypeDropdownItemDisplay = (dropdownItem: string) => { + return ( + + {dropdownItem} + + ) + } + + // model select combobox + const handleModelFilteredDropdownItems = ( + dropdownItems: Model[], + selectedItems: Model[], + inputValue: string + ) => { + const lowerCasedInputValue = inputValue.toLowerCase() + + // First filter out items that are already selected + const unselectedItems = dropdownItems.filter( + (dropdownItem) => + !selectedItems.some((selectedItem) => selectedItem.name === dropdownItem.name) + ) + + // Then filter by input value + return unselectedItems.filter((item) => item.name.toLowerCase().includes(lowerCasedInputValue)) + } + + const handleModelDropdownItemDisplay = (dropdownItem: Model) => { + if (dropdownItem.isDefault) { + return ( + + + {dropdownItem.name} + + + + + + + + + + {t('channels.modelDefault')} + + + + ) + } + return ( + + {dropdownItem.name} + + ) + } + + const handleModelSelectedItemDisplay = (selectedItem: Model) => { + if (selectedItem.isDefault) { + return ( + + + {selectedItem.name} + + + + + + + + + + + ) + } + return ( + + {selectedItem.name} + + ) + } + + const handleSetCustomModel = ( + selectedItems: Model[], + setSelectedItems: Dispatch>, + customModeName: string, + setCustomModeName: Dispatch> + ) => { + if (customModeName.trim()) { + const newModel: Model = { + name: customModeName.trim(), + isDefault: false + } + + const exists = selectedItems.some((item) => item.name === customModeName.trim()) + + if (!exists) { + setSelectedItems([...selectedItems, newModel]) + setCustomModeName('') + } + } + } + + // form schema + const schema = z.object({ + id: z.number().optional(), + type: z.number(), + name: z.string().min(1, { message: t('channels.name_required') }), + key: z.string().min(1, { message: t('channels.key_required') }), + base_url: z.string(), + models: z.array(z.string()).default([]), + model_mapping: z.record(z.string(), z.any()).default({}) + }) + + const id = channelInfo?.id + type FormData = z.infer + + const { + register, + handleSubmit, + reset, + setValue, + watch, + formState: { errors }, + control + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + id: id, + type: undefined, + name: '', + key: '', + base_url: '', + models: [], + model_mapping: {} + }, + mode: 'onChange', + reValidateMode: 'onChange' + }) + + useEffect(() => { + if (channelInfo) { + const { id, type, name, key, base_url, models, model_mapping } = channelInfo + reset({ id, type, name, key, base_url, models, model_mapping }) + } + }, [channelInfo]) + + const resetModalState = () => { + reset() + } + + const createChannelMutation = useMutation({ + mutationFn: createChannel, + onSuccess: () => { + message({ + title: t('channels.createSuccess'), + status: 'success' + }) + } + }) + + const updateChannelMutation = useMutation({ + mutationFn: (data: FormData) => + updateChannel( + { + type: data.type, + name: data.name, + key: data.key, + base_url: data.base_url, + models: data.models, + model_mapping: data.model_mapping + }, + data.id!.toString() + ), + onSuccess: () => { + message({ + title: t('channels.updateSuccess'), + status: 'success' + }) + } + }) + + const onValidate = async (data: FormData) => { + try { + switch (operationType) { + case 'create': + await createChannelMutation.mutateAsync({ + type: data.type, + name: data.name, + key: data.key, + base_url: data.base_url, + models: data.models, + model_mapping: data.model_mapping + }) + break + case 'update': + await updateChannelMutation.mutateAsync(data) + break + } + queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannels] }) + queryClient.invalidateQueries({ queryKey: [QueryKey.GetDefaultModelAndModeMapping] }) + queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannelTypeNames] }) + queryClient.invalidateQueries({ queryKey: [QueryKey.GetAllChannelModes] }) + resetModalState() + onClose() + } catch (error) { + switch (operationType) { + case 'create': + message({ + title: t('channels.createFailed'), + status: 'error', + position: 'top', + duration: 2000, + isClosable: true, + description: error instanceof Error ? error.message : t('channels.createFailed') + }) + break + case 'update': + message({ + title: t('channels.updateFailed'), + status: 'error', + position: 'top', + duration: 2000, + isClosable: true, + description: error instanceof Error ? error.message : t('channels.updateFailed') + }) + break + } + } + } + + const onInvalid = (errors: FieldErrors): void => { + const firstErrorMessage = Object.values(errors)[0]?.message + if (firstErrorMessage) { + message({ + title: firstErrorMessage as string, + status: 'error', + position: 'top', + duration: 2000, + isClosable: true + }) + } + } + + const onSubmit = handleSubmit(onValidate, onInvalid) + + return ( + + {isOpen && + (isBuiltInSupportModelsLoading || + isDefaultEnabledModelsLoading || + isChannelTypeNamesLoading || + !builtInSupportModels || + !defaultEnabledModels || + !channelTypeNames ? ( + <> + + + + + + {operationType === 'create' ? t('channels.create') : t('channels.edit')} + + + + + +
+ +
+
+
+ + ) : ( + <> + + + {/* header */} + + + + {operationType === 'create' ? t('channels.create') : t('channels.edit')} + + + + + {/* body */} + + + + + + {t('channelsForm.name')} + + + + {errors.name && {errors.name.message}} + + + + + { + const availableChannels = Object.entries(channelTypeNames) + .filter(([channel]) => channel in builtInSupportModels) + .map(([_, name]) => name) + + const initSelectedItem = field.value + ? channelTypeNames[String(field.value) as ChannelType] + : undefined + + return ( + + dropdownItems={availableChannels} + initSelectedItem={initSelectedItem} + setSelectedItem={(channelName: string) => { + if (channelName) { + const channelType = Object.entries(channelTypeNames).find( + ([_, name]) => name === channelName + )?.[0] + + if (channelType) { + const numericChannel = Number(channelType) + field.onChange(numericChannel) + setValue('models', []) + setValue('model_mapping', {}) + } + } + }} + handleDropdownItemFilter={handleModelTypeDropdownItemFilter} + handleDropdownItemDisplay={handleModelTypeDropdownItemDisplay} + /> + ) + }} + /> + {errors.type && {errors.type.message}} + + + + { + const channelType = String(watch('type')) as ChannelType + + const builtInModes = + builtInSupportModels[channelType]?.map((mode) => mode.model) || [] + const defaultModes = defaultEnabledModels.models[channelType] || [] + + const allModes: Model[] = builtInModes.map((modeName) => ({ + name: modeName, + isDefault: defaultModes.includes(modeName) + })) + + const selectedModels: Model[] = field.value.map((modeName) => ({ + name: modeName, + isDefault: defaultModes.includes(modeName) + })) + + return ( + + dropdownItems={allModes} + selectedItems={selectedModels} + setSelectedItems={(models) => { + field.onChange((models as Model[]).map((m) => m.name)) + }} + handleFilteredDropdownItems={handleModelFilteredDropdownItems} + handleDropdownItemDisplay={handleModelDropdownItemDisplay} + handleSelectedItemDisplay={handleModelSelectedItemDisplay} + handleSetCustomSelectedItem={handleSetCustomModel} + /> + ) + }} + /> + {errors.models && {errors.models.message}} + + + + { + const channelType = String(watch('type')) as ChannelType + + const selectedModels = watch('models') + const defaultModes = defaultEnabledModels.models[channelType] || [] + + const covertedSelectedModels: Model[] = selectedModels.map((modeName) => ({ + name: modeName, + isDefault: defaultModes.includes(modeName) + })) + return ( + { + field.onChange(mapping) + }} + /> + ) + }} + /> + {errors.model_mapping?.message && ( + {errors.model_mapping.message.toString()} + )} + + + + + + {t('channelsForm.key')} + + + + {errors.key && {errors.key.message}} + + + + + + + {t('channelsForm.base_url')} + + + + {errors.base_url && ( + {errors.base_url.message} + )} + + + + + + + + + + ))} +
+ ) +} + +export default UpdateChannelModal diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx index d84c050af4ec..c09c4b2ca100 100644 --- a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx @@ -1,17 +1,292 @@ 'use client' -import { Flex } from '@chakra-ui/react' +import { Button, Flex, Text, useDisclosure, useToast } from '@chakra-ui/react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import ChannelTable from './components/ChannelTable' +import UpdateChannelModal from './components/UpdateChannelModal' +import { useState, useRef } from 'react' +import { ChannelInfo } from '@/types/admin/channels/channelInfo' +import { getAllChannels, uploadChannels } from '@/api/platform' +import { QueryKey } from '@/types/query-key' +import { downloadJson } from '@/utils/common' +import { useMessage } from '@sealos/ui' export default function DashboardPage() { + const { isOpen, onOpen, onClose } = useDisclosure() + const [operationType, setOperationType] = useState<'create' | 'update'>('create') + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const [exportData, setExportData] = useState([]) + const fileInputRef = useRef(null) + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const queryClient = useQueryClient() + + const { + data: allChannels, + isFetching: isAllChannelsFetching, + refetch + } = useQuery({ + queryKey: [QueryKey.GetAllChannels], + queryFn: getAllChannels, + refetchOnReconnect: false, + enabled: false + }) + + const uploadMutation = useMutation({ + mutationFn: uploadChannels + }) + + const handleExport = async () => { + if (exportData.length === 0) { + const result = await refetch() + const dataToExport = result.data || [] + downloadJson(dataToExport, 'channels') + } else { + downloadJson(exportData, 'channels') + } + } + + const handleImport = () => { + fileInputRef.current?.click() + } + + const handleFileChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file) return + + const formData = new FormData() + formData.append('file', file) + + try { + await uploadMutation.mutateAsync(formData) + message({ + title: t('dashboard.importSuccess'), + status: 'success', + duration: 3000, + isClosable: true + }) + queryClient.invalidateQueries([QueryKey.GetChannels]) + queryClient.invalidateQueries([QueryKey.GetChannelTypeNames]) + } catch (error) { + console.error('Import error:', error) + message({ + title: t('dashboard.importError'), + status: 'error', + duration: 3000, + isClosable: true + }) + } finally { + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + } + return ( - <> - - Dashboard + + + {/* header */} + + + {t('dashboard.title')} + + + + + <> + + + + + + + {/* header end */} + {/* table */} + + {/* modal */} + - - + ) } - -function ChannelList() { - return ChannelList -} diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/CommonConfig.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/CommonConfig.tsx new file mode 100644 index 000000000000..b80c7ead9877 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/CommonConfig.tsx @@ -0,0 +1,166 @@ +'use client' +import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query' +import { Switch } from '@chakra-ui/react' +import { EditableText } from './EditableText' +import { getOption, updateOption } from '@/api/platform' +import { useMessage } from '@sealos/ui' +import { QueryKey } from '@/types/query-key' +import { useState } from 'react' +import { produce } from 'immer' + +export enum CommonConfigKey { + GlobalApiRateLimitNum = 'GlobalApiRateLimitNum', + DisableServe = 'DisableServe', + RetryTimes = 'RetryTimes', + GroupMaxTokenNum = 'GroupMaxTokenNum' +} + +type CommonConfig = { + [CommonConfigKey.GlobalApiRateLimitNum]: string + [CommonConfigKey.DisableServe]: string + [CommonConfigKey.RetryTimes]: string + [CommonConfigKey.GroupMaxTokenNum]: string +} + +const CommonConfig = () => { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const queryClient = useQueryClient() + + const [commonConfig, setCommonConfig] = useState(() => + produce({} as CommonConfig, (draft) => { + draft.GlobalApiRateLimitNum = '' + draft.DisableServe = '' + draft.RetryTimes = '' + draft.GroupMaxTokenNum = '' + }) + ) + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const { isLoading: isOptionLoading, data: optionData } = useQuery({ + queryKey: [QueryKey.GetCommonConfig], + queryFn: () => getOption(), + onSuccess: (data) => { + if (!data) return + + setCommonConfig( + produce(commonConfig, (draft) => { + draft.GlobalApiRateLimitNum = data.GlobalApiRateLimitNum || '' + draft.DisableServe = data.DisableServe || '' + draft.RetryTimes = data.RetryTimes || '' + draft.GroupMaxTokenNum = data.GroupMaxTokenNum || '' + }) + ) + } + }) + + const updateOptionMutation = useMutation({ + mutationFn: (params: { key: string; value: string }) => updateOption(params), + onSuccess: () => { + message({ + title: t('globalConfigs.saveCommonConfigSuccess'), + status: 'success' + }) + queryClient.invalidateQueries({ queryKey: [QueryKey.GetCommonConfig] }) + }, + onError: () => { + message({ + title: t('globalConfigs.saveCommonConfigFailed'), + status: 'error' + }) + } + }) + + const updateConfigField = (field: CommonConfigKey, value: string) => { + setCommonConfig( + produce((draft) => { + draft[field] = value + }) + ) + updateOptionMutation.mutate({ key: field, value }) + } + + const handleDisableServeChange = (checked: boolean) => { + const value = checked ? 'true' : 'false' + updateConfigField(CommonConfigKey.DisableServe, value) + } + + return ( + /* + h = 72px + 20px + 60px = 152px + EditableText (24px × 3) = 72px + Switch container (20px) = 20px + gap (20px × 3) = 60px + */ + + {/* title */} + + + {t('globalConfigs.common_config')} + + + {/* -- title end */} + + {/* config */} + + + {/* QPM Limit */} + updateConfigField(CommonConfigKey.GlobalApiRateLimitNum, value)} + flexProps={{ h: '24px' }} + /> + + {/* Pause Service */} + + {t('global_configs.pause_service')} + handleDisableServeChange(e.target.checked)} + /> + + + {/* Retry Count */} + updateConfigField(CommonConfigKey.RetryTimes, value)} + flexProps={{ h: '24px' }} + /> + + {/* Max Token */} + updateConfigField(CommonConfigKey.GroupMaxTokenNum, value)} + flexProps={{ h: '24px' }} + /> + + + {/* -- config end */} + + ) +} + +export default CommonConfig diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/EditableText.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/EditableText.tsx new file mode 100644 index 000000000000..2ce5660629f5 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/EditableText.tsx @@ -0,0 +1,185 @@ +'use client' +import React, { useState } from 'react' +import { + Flex, + Text, + Button, + Input, + useDisclosure, + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, + HStack, + FlexProps, + Box +} from '@chakra-ui/react' +import { CheckIcon, CloseIcon } from '@chakra-ui/icons' + +interface EditableTextProps { + value: string | number + label: string + onSubmit: (value: string) => void + flexProps?: FlexProps +} + +export const EditableText = ({ value, label, onSubmit, flexProps }: EditableTextProps) => { + const [editValue, setEditValue] = useState(String(value)) + const { isOpen, onOpen, onClose } = useDisclosure() + + const handleSubmit = () => { + onSubmit(editValue) + onClose() + } + + const handleCancel = () => { + // 关闭时 恢复到传递来的初始值 + setEditValue(String(value)) + onClose() + } + + return ( + + + {label} + + + + + + {value} + + + + + + + + setEditValue(e.target.value)} + minW="0" + w="full" + h="28px" + borderRadius="6px" + border="1px solid var(--Gray-Modern-200, #E8EBF0)" + bgColor="white" + _hover={{ borderColor: 'grayModern.300' }} + _focus={{ borderColor: 'grayModern.300' }} + _focusVisible={{ borderColor: 'grayModern.300' }} + _active={{ borderColor: 'grayModern.300' }} + autoFocus + /> + + + + + + + + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/ModelConfig.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/ModelConfig.tsx new file mode 100644 index 000000000000..1d26ca7c6a85 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/components/ModelConfig.tsx @@ -0,0 +1,682 @@ +'use client' +import { Button, Flex, Text, FormControl, VStack, Skeleton } from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { MultiSelectCombobox } from '@/components/common/MultiSelectCombobox' +import { SingleSelectCombobox } from '@/components/common/SingleSelectCombobox' +import { useForm, Controller, FieldErrors, FieldErrorsImpl, FieldError } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { + batchOption, + getChannelBuiltInSupportModels, + getChannelTypeNames, + getOption +} from '@/api/platform' +import { SetStateAction, Dispatch, useEffect, useState } from 'react' +import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query' +import ConstructMappingComponent from '@/components/common/ConstructMappingComponent' +import { DefaultChannelModel, DefaultChannelModelMapping } from '@/types/admin/option' +import { ChannelType } from '@/types/admin/channels/channelInfo' +import { QueryKey } from '@/types/query-key' +import { useMessage } from '@sealos/ui' +import { BatchOptionData } from '@/types/admin/option' + +const ModelConfig = () => { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const queryClient = useQueryClient() + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const [allSupportChannel, setAllSupportChannel] = useState([]) + const [allSupportChannelWithMode, setAllSupportChannelWithMode] = useState<{ + [key in ChannelType]: string[] + }>({}) + + const [defaultModel, setDefaultModel] = useState({}) + const [defaultModelMapping, setDefaultModelMapping] = useState({}) + + const { isLoading: isChannelTypeNamesLoading, data: channelTypeNames } = useQuery({ + queryKey: [QueryKey.GetChannelTypeNames], + queryFn: () => getChannelTypeNames() + }) + + const { isLoading: isBuiltInSupportModelsLoading, data: builtInSupportModels } = useQuery({ + queryKey: [QueryKey.GetAllChannelModes], + queryFn: () => getChannelBuiltInSupportModels() + }) + + const { isLoading: isOptionLoading, data: optionData } = useQuery({ + queryKey: [QueryKey.GetOption], + queryFn: () => getOption(), + onSuccess: (data) => { + if (!data) return + + const defaultModels: DefaultChannelModel = JSON.parse(data.DefaultChannelModels) + const defaultModelMappings: DefaultChannelModelMapping = JSON.parse( + data.DefaultChannelModelMapping + ) + + setDefaultModel(defaultModels) + setDefaultModelMapping(defaultModelMappings) + } + }) + + useEffect(() => { + if (!channelTypeNames || !builtInSupportModels) return + + // 1. 处理 allSupportChannel + const supportedChannels = Object.entries(channelTypeNames) + .filter(([channel]) => channel in builtInSupportModels) + .map(([_, name]) => name) + + setAllSupportChannel(supportedChannels) + + // 2. 处理 allSupportChannelWithMode + // 渠道类型可能出现在 channelTypeNames 中,但不在 builtInSupportModels 中,所以需要过滤 + // 但在 builtInSupportModels 中,则一定在 channelTypeNames 中,所以 以 builtInSupportModels 为主 + const channelWithModes = Object.entries(channelTypeNames) + .filter(([channelType, _]) => channelType in builtInSupportModels) + .reduce((acc, [channelType, channelName]) => { + const modelInfos = builtInSupportModels[channelType as ChannelType] || [] + const models = [...new Set(modelInfos.map((info) => info.model))] + + return { + ...acc, + [channelType]: models + } + }, {} as { [key in ChannelType]: string[] }) + + setAllSupportChannelWithMode(channelWithModes) + }, [channelTypeNames, builtInSupportModels]) + + // form schema + const itemSchema = z.object({ + type: z.number(), + defaultMode: z.array(z.string()), + defaultModeMapping: z + .record(z.string(), z.string()) + .refine((mapping) => { + // 检查所有值不能为空字符串 + return Object.values(mapping).every((value) => value.trim() !== '') + }) + .default({}) + }) + + const schema = z.array(itemSchema) + + type ConfigItem = z.infer + type FormData = ConfigItem[] + + const { + register, + handleSubmit, + reset, + setValue, + watch, + formState: { errors }, + control + } = useForm({ + resolver: zodResolver(schema), + defaultValues: [], + mode: 'onChange', + reValidateMode: 'onChange' + }) + + useEffect(() => { + if (!defaultModel || !defaultModelMapping) return + // Only proceed if both defaultModel and defaultModelMapping are available + if (Object.keys(defaultModel).length === 0) return + + // Transform the data into form format + const formData: FormData = Object.entries(defaultModel).map(([channelType, modes]) => { + return { + type: Number(channelType), + defaultMode: modes || [], // Using first mode as default + defaultModeMapping: defaultModelMapping[channelType as ChannelType] || {} + } + }) + + // Reset form with the new values + reset(formData) + }, [defaultModel, defaultModelMapping, reset]) + + const handleAddDefaultModel = () => { + const newItem = { + type: undefined, // Default type value + defaultMode: [], + defaultModeMapping: {} + } + + // Get current form values + const currentValues = watch() + // Create new array with new item at the beginning + const newValues = [newItem, ...Object.values(currentValues)] + // Reset form with new values + reset(newValues) + } + + const formValues = watch() + + const formValuesArray: FormData = Array.isArray(formValues) + ? formValues + : Object.values(formValues) + + const batchOptionMutation = useMutation({ + mutationFn: batchOption, + onSuccess: () => { + message({ + title: t('globalConfigs.saveDefaultModelSuccess'), + status: 'success' + }) + } + }) + + const transformFormDataToConfig = (formData: FormData): BatchOptionData => { + // 初始化两个对象 + const defaultChannelModelMapping: Record> = {} + const defaultChannelModels: Record = {} + + // 遍历 FormData + formData.forEach((item) => { + const type = item.type.toString() + + // 处理 DefaultChannelModelMapping + if (Object.keys(item.defaultModeMapping).length > 0) { + defaultChannelModelMapping[type] = item.defaultModeMapping + } + + // 处理 DefaultChannelModels + if (item.defaultMode.length > 0) { + defaultChannelModels[type] = item.defaultMode + } + }) + + return { + // 转换为 JSON 字符串 + DefaultChannelModelMapping: JSON.stringify(defaultChannelModelMapping), + DefaultChannelModels: JSON.stringify(defaultChannelModels) + } + } + + const resetForm = () => { + reset() + } + + type FieldErrorType = + | FieldError + | FieldErrorsImpl<{ + type: number + defaultMode: string[] + defaultModeMapping: Record + }> + + const getFirstErrorMessage = (errors: FieldErrors): string => { + // Iterate through top-level errors + for (const index in errors) { + const fieldError = errors[index] as FieldErrorType + if (!fieldError) continue + + // Check if error is an object + if (typeof fieldError === 'object') { + // If it has a direct message property + if ('message' in fieldError && fieldError.message) { + return `Item ${Number(index) + 1}: ${fieldError.message}` + } + + // Iterate through nested field errors + const errorKeys = Object.keys(fieldError) as Array + for (const fieldName of errorKeys) { + const nestedError = fieldError[fieldName] + if (nestedError && typeof nestedError === 'object' && 'message' in nestedError) { + // Map field names to their display labels + const fieldLabel = + { + type: 'Type', + defaultMode: 'Default Mode', + defaultModeMapping: 'Model Mapping' + }[fieldName as string] || fieldName + + return `Item ${Number(index) + 1} ${fieldLabel}: ${nestedError.message}` + } + } + } + } + return 'Form validation failed' + } + + const onValidate = async (data: FormData) => { + try { + const batchOptionData: BatchOptionData = transformFormDataToConfig(data) + await batchOptionMutation.mutateAsync(batchOptionData) + + queryClient.invalidateQueries({ queryKey: [QueryKey.GetOption] }) + queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannelTypeNames] }) + queryClient.invalidateQueries({ queryKey: [QueryKey.GetAllChannelModes] }) + resetForm() + } catch (error) { + message({ + title: t('globalConfigs.saveDefaultModelFailed'), + status: 'error', + position: 'top', + duration: 2000, + isClosable: true, + description: + error instanceof Error ? error.message : t('globalConfigs.saveDefaultModelFailed') + }) + console.error(error) + } + } + + const onInvalid = (errors: FieldErrors): void => { + console.error('errors', errors) + + const errorMessage = getFirstErrorMessage(errors) + + message({ + title: errorMessage, + status: 'error', + position: 'top', + duration: 2000, + isClosable: true + }) + } + + const onSubmit = handleSubmit(onValidate, onInvalid) + return ( + /* + 顶级 Flex 容器的高度: calc(100vh - 16px - 24px - 12px - 32px - 36px) + ModelConfig 的高度: calc(100vh - 16px - 24px - 12px - 32px - 36px)- CommonConfig 的高度(152px) -两个 gap 的高度(36px × 2 = 72px) + = calc(100vh - 16px - 24px - 12px - 32px - 36px - 152px - 72px) + = calc(100vh - 344px) + */ + + {/* title */} + + + {t('globalConfigs.model_config')} + + + {/* -- title end */} + + {/* config */} + + {/* add default model */} + + + {t('globalConfigs.defaultModel')} + + + + + + + + {/* default model */} + + + {isChannelTypeNamesLoading || + isBuiltInSupportModelsLoading || + isOptionLoading || + formValuesArray?.length === 0 ? ( + + ) : ( + formValuesArray && + formValuesArray.length > 0 && + channelTypeNames && + formValuesArray.map((value, index) => { + return ( + + + + + + { + // Get current form types + const currentFormTypes = Object.values(formValues) + .map((item) => String(item.type)) + .filter( + (type): type is ChannelType => + type !== undefined && type in channelTypeNames + ) + .map((type) => channelTypeNames[type]) + + // Filter available types + const availableTypes = allSupportChannel.filter( + (channelType) => + !currentFormTypes.includes(channelType) || + // 避免编辑时当前选中值"消失"的问题,即当前选择项也包含 + (field.value && + channelTypeNames[String(field.value) as ChannelType] === + channelType) + ) + + const initSelectedItem = field.value + ? channelTypeNames[String(field.value) as ChannelType] + : undefined + + return ( + + dropdownItems={availableTypes} + initSelectedItem={initSelectedItem} + setSelectedItem={(channelName: string) => { + if (channelName) { + const channelType = Object.entries(channelTypeNames).find( + ([_, name]) => name === channelName + )?.[0] + + if (channelType) { + const defaultModelField = + defaultModel[channelType as ChannelType] + const defaultModelMappingField = + defaultModelMapping[channelType as ChannelType] + + field.onChange(Number(channelType)) + setValue(`${index}.defaultMode`, defaultModelField || []) + setValue( + `${index}.defaultModeMapping`, + defaultModelMappingField || {} + ) + } + } + }} + handleDropdownItemFilter={( + dropdownItems: string[], + inputValue: string + ) => { + const lowerCasedInput = inputValue.toLowerCase() + return dropdownItems.filter( + (item) => + !inputValue || item.toLowerCase().includes(lowerCasedInput) + ) + }} + handleDropdownItemDisplay={(item: string) => ( + + {item} + + )} + /> + ) + }} + /> + + + + { + // Get the current type value from the form + const currentType = watch(`${index}.type`) + const dropdownItems = currentType + ? allSupportChannelWithMode[String(currentType) as ChannelType] || [] + : [] + + const handleSetCustomModel = ( + selectedItems: string[], + setSelectedItems: Dispatch>, + customModeName: string, + setCustomModeName: Dispatch> + ) => { + if (customModeName.trim()) { + const exists = field.value.some( + (item) => item === customModeName.trim() + ) + + if (!exists) { + field.onChange([...field.value, customModeName.trim()]) + setCustomModeName('') + } + } + } + + const handleModelFilteredDropdownItems = ( + dropdownItems: string[], + selectedItems: string[], + inputValue: string + ) => { + const lowerCasedInputValue = inputValue.toLowerCase() + + return dropdownItems.filter( + (item) => + !selectedItems.includes(item) && + item.toLowerCase().includes(lowerCasedInputValue) + ) + } + + return ( + + dropdownItems={dropdownItems || []} + selectedItems={field.value || []} // Use field.value for selected items + setSelectedItems={(models) => { + field.onChange(models) + }} + handleFilteredDropdownItems={handleModelFilteredDropdownItems} + handleDropdownItemDisplay={(item) => ( + + {item} + + )} + handleSelectedItemDisplay={(item) => ( + + {item} + + )} + handleSetCustomSelectedItem={handleSetCustomModel} + /> + ) + }} + /> + + + + { + const defaultMode = watch(`${index}.defaultMode`) + const defaultModeMapping = watch(`${index}.defaultModeMapping`) + + return ( + { + field.onChange(mapping) + }} + /> + ) + }} + /> + + + + ) + }) + )} + + + {/* -- config end */} + + ) +} + +export default ModelConfig diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/page.tsx new file mode 100644 index 000000000000..876e2bf812fb --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/global-configs/page.tsx @@ -0,0 +1,259 @@ +'use client' +import { Button, Divider, Flex, Text, useDisclosure } from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import CommonConfig from './components/CommonConfig' +import ModelConfig from './components/ModelConfig' +import { getOption, uploadOptions } from '@/api/platform' +import { QueryKey } from '@/types/query-key' +import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query' +import { useMessage } from '@sealos/ui/src/components' +import { useRef } from 'react' +import { downloadJson } from '@/utils/common' + +export default function GlobalConfigPage() { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + + const fileInputRef = useRef(null) + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const queryClient = useQueryClient() + + const { + isFetching: isOptionFetching, + data: optionData, + refetch + } = useQuery({ + queryKey: [QueryKey.GetOption], + queryFn: () => getOption(), + refetchOnReconnect: false, + enabled: false + }) + + const uploadMutation = useMutation({ + mutationFn: uploadOptions + }) + + const handleExport = async () => { + const result = await refetch() + const dataToExport = result.data || [] + downloadJson(dataToExport, 'global_configs') + } + + const handleImport = () => { + fileInputRef.current?.click() + } + + const handleFileChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file) return + + const formData = new FormData() + formData.append('file', file) + + try { + await uploadMutation.mutateAsync(formData) + message({ + title: t('dashboard.importSuccess'), + status: 'success', + duration: 3000, + isClosable: true + }) + queryClient.invalidateQueries([QueryKey.GetOption]) + queryClient.invalidateQueries([QueryKey.GetCommonConfig]) + } catch (error) { + console.error('Import error:', error) + message({ + title: t('dashboard.importError'), + status: 'error', + duration: 3000, + isClosable: true + }) + } finally { + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + } + + return ( + + + {/* header */} + + + {t('global_configs.title')} + + + + <> + + + + + + + {/* header end */} + + {/* config */} + {/* + 100vh - 16px (父元素上下padding: 4px + 12px) + 24px (顶部padding: pt="24px") + 12px (底部padding: pb="12px") + 32px (header高度) + 36px (flex gap间距) + */} + + + + + + {/* -- config end */} + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/global-logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/global-logs/page.tsx new file mode 100644 index 000000000000..2c3125575262 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/global-logs/page.tsx @@ -0,0 +1,436 @@ +'use client' + +import { Box, Flex, Text, Button, Icon, Input } from '@chakra-ui/react' +import { CurrencySymbol, MyTooltip } from '@sealos/ui' +import { useMemo, useState } from 'react' + +import { getGlobalLogs, getEnabledMode } from '@/api/platform' +import { useTranslationClientSide } from '@/app/i18n/client' +import SelectDateRange from '@/components/common/SelectDateRange' +import SwitchPage from '@/components/common/SwitchPage' +import { BaseTable } from '@/components/table/BaseTable' +import { useI18n } from '@/providers/i18n/i18nContext' +import { GlobalLogItem } from '@/types/user/logs' +import { useQuery } from '@tanstack/react-query' +import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import { QueryKey } from '@/types/query-key' +import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle' +import { useBackendStore } from '@/store/backend' + +export default function Home(): React.JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { currencySymbol } = useBackendStore() + + const [startTime, setStartTime] = useState(() => { + const currentDate = new Date() + currentDate.setMonth(currentDate.getMonth() - 1) + return currentDate + }) + const [endTime, setEndTime] = useState(new Date()) + const [groupId, setGroupId] = useState('') + const [name, setName] = useState('') + const [modelName, setModelName] = useState('') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [logData, setLogData] = useState([]) + const [total, setTotal] = useState(0) + + const { data: models = [] } = useQuery([QueryKey.GetEnabledModels], () => getEnabledMode()) + + const { isLoading } = useQuery( + [QueryKey.GetGlobalLogs, page, pageSize, name, modelName, startTime, endTime, groupId], + () => + getGlobalLogs({ + page, + perPage: pageSize, + token_name: name, + model_name: modelName, + start_timestamp: startTime.getTime().toString(), + end_timestamp: endTime.getTime().toString(), + group_id: groupId + }), + { + onSuccess: (data) => { + if (!data?.logs) { + setLogData([]) + setTotal(0) + return + } + setLogData(data?.logs || []) + setTotal(data?.total || 0) + } + } + ) + + const columns = useMemo[]>(() => { + return [ + { + header: t('GlobalLogs.groupId'), + accessorKey: 'group' + }, + { + header: t('GlobalLogs.tokenName'), + accessorKey: 'token_name' + }, + { + header: t('logs.model'), + accessorKey: 'model' + }, + { + header: t('GlobalLogs.channel'), + accessorKey: 'channel' + }, + { + header: t('logs.prompt_tokens'), + accessorKey: 'prompt_tokens' + }, + { + header: t('logs.completion_tokens'), + accessorKey: 'completion_tokens' + }, + + { + header: t('logs.status'), + accessorFn: (row) => (row.code === 200 ? t('logs.success') : t('logs.failed')), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + + {value} + + ) + }, + id: 'status' + }, + { + header: t('logs.time'), + accessorFn: (row) => new Date(row.created_at).toLocaleString(), + id: 'created_at' + }, + { + accessorKey: 'used_amount', + id: 'used_amount', + header: () => { + return ( + + + + + {t('logs.total_price')} + + + + + + ) + } + } + ] + }, []) + + const table = useReactTable({ + data: logData, + columns, + getCoreRowModel: getCoreRowModel() + }) + + return ( + + + {/* -- header */} + + + + {t('logs.call_log')} + + + + + {/* -- the first row */} + + + + {t('GlobalLogs.keyName')} + + setName(e.target.value)} + /> + + + + + {t('logs.modal')} + + + + dropdownItems={['all', ...models.map((item) => item.model)]} + setSelectedItem={(value) => { + if (value === 'all') { + setModelName('') + } else { + setModelName(value) + } + }} + handleDropdownItemFilter={(dropdownItems, inputValue) => { + const lowerCasedInput = inputValue.toLowerCase() + return dropdownItems.filter( + (item) => !inputValue || item.toLowerCase().includes(lowerCasedInput) + ) + }} + handleDropdownItemDisplay={(dropdownItem) => { + return ( + + {dropdownItem} + + ) + }} + flexProps={{ w: '500px' }} + placeholder={t('GlobalLogs.selectModel')} + /> + + + + {/* -- the first row end */} + + {/* -- the second row */} + + + + {t('GlobalLogs.groupId')} + + setGroupId(e.target.value)} + /> + + + + + {t('logs.time')} + + + + + {/* -- the second row end */} + + + {/* -- header end */} + + {/* -- table */} + + + setPage(idx)} + /> + + {/* -- table end */} + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx index 6454303cbdd4..f3c187f8c9e1 100644 --- a/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx @@ -1,17 +1,17 @@ -import { Flex } from '@chakra-ui/react' +import { Box, Flex } from '@chakra-ui/react' -export default function AdminLayout({ children }: { children: React.ReactNode }) { +import SideBar from '@/components/admin/Sidebar' + +export default function UserLayout({ children }: { children: React.ReactNode }) { return ( - - + + + + + {/* Main Content */} + {children} - + ) } diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/ns-manager/page.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/ns-manager/page.tsx new file mode 100644 index 000000000000..54952dd2dc43 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/ns-manager/page.tsx @@ -0,0 +1,556 @@ +'use client' + +import { + Box, + Flex, + Text, + Button, + Icon, + Input, + MenuItem, + MenuList, + Menu, + MenuButton +} from '@chakra-ui/react' +import { CurrencySymbol } from '@sealos/ui' +import { useMemo, useState } from 'react' + +import { deleteGroup, getGroups, updateGroupQpm, updateGroupStatus } from '@/api/platform' +import { useTranslationClientSide } from '@/app/i18n/client' +import SwitchPage from '@/components/common/SwitchPage' +import { BaseTable } from '@/components/table/BaseTable' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + ColumnDef, + getCoreRowModel, + useReactTable, + createColumnHelper +} from '@tanstack/react-table' +import { QueryKey } from '@/types/query-key' +import { useBackendStore } from '@/store/backend' +import { GroupInfo, GroupStatus } from '@/types/admin/group' +import { useMessage } from '@sealos/ui' +import { EditableTextNoLable } from '@/components/common/EditableTextNoLable' + +export default function Home(): React.JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { currencySymbol } = useBackendStore() + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const queryClient = useQueryClient() + + const [groupId, setGroupId] = useState('') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [groupData, setGroupData] = useState([]) + const [total, setTotal] = useState(0) + + const { isLoading } = useQuery( + [QueryKey.GetGroups, page, pageSize, groupId], + () => + getGroups({ + page, + perPage: pageSize, + keyword: groupId + }), + { + onSuccess: (data) => { + if (!data?.groups) { + setGroupData([]) + setTotal(0) + return + } + setGroupData(data?.groups || []) + setTotal(data?.total || 0) + } + } + ) + + const deleteGroupMutation = useMutation(({ id }: { id: string }) => deleteGroup(id), { + onSuccess() { + message({ + status: 'success', + title: t('nsManager.deleteGroupSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + queryClient.invalidateQueries([QueryKey.GetGroups]) + }, + onError(err: any) { + message({ + status: 'warning', + title: t('nsManager.deleteGroupFailed'), + description: err?.message || t('nsManager.deleteGroupFailed'), + isClosable: true, + position: 'top' + }) + } + }) + + const updateGroupStatusMutation = useMutation( + ({ id, status }: { id: string; status: number }) => updateGroupStatus(id, status), + { + onSuccess() { + message({ + status: 'success', + title: t('nsManager.updateGroupStatusSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + queryClient.invalidateQueries([QueryKey.GetGroups]) + }, + onError(err: any) { + message({ + status: 'warning', + title: t('nsManager.updateGroupStatusFailed'), + description: err?.message || t('nsManager.updateGroupStatusFailed'), + isClosable: true, + position: 'top' + }) + } + } + ) + + const updateGroupQpmMutation = useMutation( + ({ id, qpm }: { id: string; qpm: number }) => updateGroupQpm(id, qpm), + { + onSuccess() { + message({ + status: 'success', + title: t('nsManager.updateGroupQpmSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + queryClient.invalidateQueries([QueryKey.GetGroups]) + }, + onError(err: any) { + message({ + status: 'warning', + title: t('nsManager.updateGroupQpmFailed'), + description: err?.message || t('nsManager.updateGroupQpmFailed'), + isClosable: true, + position: 'top' + }) + } + } + ) + + // Update the button click handlers in the table actions column: + const handleStatusUpdate = (id: string, currentStatus: number) => { + const newStatus = + currentStatus === GroupStatus.DISABLED ? GroupStatus.ENABLED : GroupStatus.DISABLED + updateGroupStatusMutation.mutate({ id, status: newStatus }) + } + const columnHelper = createColumnHelper() + + const columns = useMemo[]>(() => { + return [ + { + header: t('nsManager.groupId'), + accessorKey: 'id' + }, + { + header: t('nsManager.qpm'), + id: 'qpm', + cell: (info) => ( + + updateGroupQpmMutation.mutate({ + id: info.row.original.id, + qpm: Number(value) + }) + } + /> + ) + }, + { + header: t('nsManager.created_at'), + accessorFn: (row) => new Date(row.created_at).toLocaleString(), + id: 'created_at' + }, + { + header: t('nsManager.accessed_at'), + accessorFn: (row) => { + if (row.accessed_at && row.accessed_at < 0) { + return t('key.unused') + } + + return new Date(row.accessed_at).toLocaleString() + }, + id: 'accessed_at' + }, + { + header: t('nsManager.request_count'), + accessorKey: 'request_count' + }, + { + header: t('nsManager.status'), + accessorFn: (row) => + row.status === GroupStatus.ENABLED ? t('nsManager.enabled') : t('nsManager.disabled'), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + + {value} + + ) + }, + id: 'status' + }, + { + accessorKey: 'used_amount', + id: 'used_amount', + header: () => { + return ( + + + + {t('nsManager.used_amount')} + + + + + ) + } + }, + columnHelper.display({ + id: 'actions', + header: () => ( + + {t('nsManager.actions')} + + ), + cell: (info) => ( + + + + + + + + handleStatusUpdate(info.row.original.id, info.row.original.status)}> + {info.row.original.status === GroupStatus.ENABLED ? ( + <> + + + + + {t('channels.disable')} + + + ) : ( + <> + + + + + {t('channels.enable')} + + + )} + + deleteGroupMutation.mutate({ id: String(info.row.original.id) })}> + + + + + {t('channels.delete')} + + + + + ) + }) + ] + }, []) + + const table = useReactTable({ + data: groupData, + columns, + getCoreRowModel: getCoreRowModel() + }) + + return ( + + + {/* -- header */} + + + + {t('nsManager.ns_manager')} + + + + + {/* -- the first row */} + + + + {t('nsManager.groupId')} + + setGroupId(e.target.value)} + /> + + + + {/* -- the first row end */} + + + {/* -- header end */} + + {/* -- table */} + + + setPage(idx)} + /> + + {/* -- table end */} + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/home/components/RequestDataChart.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/components/RequestDataChart.tsx new file mode 100644 index 000000000000..4c6a4c586de4 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/components/RequestDataChart.tsx @@ -0,0 +1,435 @@ +import { Box, Flex, Text } from '@chakra-ui/react' +import { useEffect, useRef } from 'react' +import * as echarts from 'echarts' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { ChartDataItem } from '@/types/user/dashboard' +import { useBackendStore } from '@/store/backend' + +export default function RequestDataChart({ data }: { data: ChartDataItem[] }): React.JSX.Element { + const costChartRef = useRef(null) + const requestChartRef = useRef(null) + const costChartInstance = useRef() + const requestChartInstance = useRef() + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { currencySymbol } = useBackendStore() + + // Add helper function to determine date format + const getDateFormat = (timestamps: number[]) => { + if (timestamps.length < 2) return 'detailed' + + const timeDiff = timestamps[timestamps.length - 1] - timestamps[0] + // If time difference is more than 15 days (1296000 seconds), show daily format + return timeDiff > 1296000 ? 'daily' : 'detailed' + } + + // 初始化图表 + useEffect(() => { + if (costChartRef.current && requestChartRef.current) { + costChartInstance.current = echarts.init(costChartRef.current, undefined, { + renderer: 'svg' + }) + requestChartInstance.current = echarts.init(requestChartRef.current, undefined, { + renderer: 'svg' + }) + } + + return () => { + costChartInstance.current?.dispose() + requestChartInstance.current?.dispose() + costChartInstance.current = undefined + requestChartInstance.current = undefined + } + }, []) + + // 配置图表选项 + useEffect(() => { + if (!costChartInstance.current || !requestChartInstance.current) return + + const commonTooltipStyle: echarts.EChartsOption['tooltip'] = { + trigger: 'axis', + axisPointer: { + type: 'line', + lineStyle: { + color: '#219BF4' + } + }, + backgroundColor: 'white', + borderWidth: 0, + padding: [8, 12], + textStyle: { + color: '#111824', + fontSize: 12 + } + } + + const commonXAxis: echarts.EChartsOption['xAxis'] = { + type: 'time', + // boundaryGap: ['0%', '5%'] as [string, string], + boundaryGap: ['0%', '0%'] as [string, string], + axisLine: { + lineStyle: { + color: '#E8EBF0', + width: 2 + } + }, + splitLine: { + show: false, + lineStyle: { + color: '#DFE2EA', + type: 'dashed' as const + } + }, + axisTick: { + show: true, + length: 6, + lineStyle: { + color: '#E8EBF0', + width: 2 + } + }, + axisLabel: { + show: true, + color: '#667085', + formatter: (value: number) => { + const date = new Date(value * 1000) + const format = getDateFormat(data.map((item) => item.timestamp)) + + return date + .toLocaleString(lng, { + month: '2-digit', + day: '2-digit', + ...(format === 'detailed' && { + hour: '2-digit', + minute: '2-digit', + hour12: false + }) + }) + .replace(/\//g, '-') + }, + margin: 14, + align: 'left' + } + } + + // 成本图表配置 + const costOption: echarts.EChartsOption = { + tooltip: { + ...commonTooltipStyle, + formatter: function ( + params: + | echarts.DefaultLabelFormatterCallbackParams + | echarts.DefaultLabelFormatterCallbackParams[] + ) { + if (!params) return '' + const paramArray = Array.isArray(params) ? params : [params] + if (paramArray.length === 0) return '' + + const time = new Date((paramArray[0].value as [number, number])[0] * 1000) + const timeStr = time.toLocaleString(lng, { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + + let result = ` +
${timeStr}
+
+ ` + + const currency = + currencySymbol === 'shellCoin' + ? ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + : currencySymbol === 'cny' + ? '¥' + : '$' + + paramArray.forEach((param) => { + const value = (param.value as [number, number])[1] + const formattedValue = Number(value).toLocaleString(lng, { + minimumFractionDigits: 0, + maximumFractionDigits: 4 + }) + result += ` +
+
+ ${param.marker} + ${param.seriesName} + ${currency} +
+
${formattedValue}
+
+ ` + }) + + return result + } + }, + legend: { + show: false, + data: [t('dataDashboard.cost')], + bottom: 0 + }, + grid: { + left: 0, + right: 0, + bottom: 10, + top: 10, + containLabel: true + }, + xAxis: commonXAxis, + yAxis: { + type: 'value', + splitLine: { + show: true, + lineStyle: { + color: '#DFE2EA', + type: 'dashed' + } + }, + axisLine: { + show: false, + lineStyle: { + color: '#667085', + width: 2 + } + }, + axisLabel: { + // formatter: '${value}', + color: '#667085' + } + }, + series: [ + { + name: t('dataDashboard.cost'), + type: 'line', + smooth: true, + showSymbol: false, + data: data.map((item) => [item.timestamp, item.used_amount]), + itemStyle: { + color: '#13C4B9' + } + } + ] + } + + // 请求数图表配置 + const requestOption: echarts.EChartsOption = { + tooltip: { + ...commonTooltipStyle, + formatter: function (params) { + if (!params) return '' + const paramArray = Array.isArray(params) ? params : [params] + if (paramArray.length === 0) return '' + + const time = new Date((paramArray[0].value as [number, number])[0] * 1000) + const timeStr = time.toLocaleString(lng, { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + + let result = ` +
${timeStr}
+
+ ` + + paramArray.forEach((param) => { + const value = (param.value as [number, number])[1] + result += ` +
+
+ ${param.marker} + ${param.seriesName} +
+
${value}
+
+ ` + }) + + return result + } + }, + legend: { + data: [t('dataDashboard.callCount'), t('dataDashboard.exceptionCount')], + bottom: 10 + }, + grid: { + left: 0, + right: 0, + bottom: 60, + top: 10, + containLabel: true + }, + xAxis: commonXAxis, + yAxis: { + type: 'value', + splitLine: { + show: true, + lineStyle: { + color: '#DFE2EA', + type: 'dashed' + } + }, + axisLine: { + show: false, + lineStyle: { + color: '#667085', + width: 2 + } + }, + axisLabel: { + color: '#667085' + } + }, + series: [ + { + name: t('dataDashboard.callCount'), + type: 'line', + smooth: true, + showSymbol: false, + data: data.map((item) => [item.timestamp, item.request_count]), + itemStyle: { + color: '#11B6FC' + } + }, + { + name: t('dataDashboard.exceptionCount'), + type: 'line', + smooth: true, + showSymbol: false, + data: data.map((item) => [item.timestamp, item.exception_count]), + itemStyle: { + color: '#FDB022' + } + } + ] + } + + // 设置图表选项 + costChartInstance.current.setOption(costOption) + requestChartInstance.current.setOption(requestOption) + + // 图表联动 + costChartInstance.current.group = 'request-data' + requestChartInstance.current.group = 'request-data' + echarts.connect('request-data') + }, [data, t, lng]) + + // 处理窗口大小变化 + useEffect(() => { + const handleResize = () => { + costChartInstance.current?.resize() + requestChartInstance.current?.resize() + } + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + return ( + + + + {t('dataDashboard.cost')} + + + + + + {t('dataDashboard.callCount')} + + + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx index e43a476d36ac..f3586b8ee5f7 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -1,41 +1,691 @@ -import { Flex } from '@chakra-ui/react' +'use client' -import KeyList from '@/components/user/KeyList' -import ModelList from '@/components/user/ModelList' +import { Box, Flex, Text, Button, Center } from '@chakra-ui/react' +import { CurrencySymbol, MySelect } from '@sealos/ui' +import { useState } from 'react' -export default function Home(): JSX.Element { - return ( - - - - +import { getDashboardData } from '@/api/platform' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { UseQueryResult, useQuery } from '@tanstack/react-query' +import { QueryKey } from '@/types/query-key' +import { useBackendStore } from '@/store/backend' +import RequestDataChart from './components/RequestDataChart' +import { DashboardResponse } from '@/types/user/dashboard' + +export default function Home(): React.JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { currencySymbol } = useBackendStore() + + const [tokenName, setTokenName] = useState('') + const [model, setModel] = useState('') + const [type, setType] = useState<'week' | 'day' | 'two_week' | 'month'>('week') // default is week + + const { data: dashboardData, isLoading }: UseQueryResult = useQuery( + [QueryKey.GetDashboardData, type, tokenName, model], + () => + getDashboardData({ + type, + ...(tokenName && { token_name: tokenName }), + ...(model && { model }) + }) + ) + return ( + - + w="full" + flex="1"> + {/* -- header */} + + + + {t('dataDashboard.title')} + + + ({ + value: token, + label: token + })) || []) + ]} + placeholder={t('dataDashboard.selectToken')} + onchange={(token: string) => { + if (token === 'all') { + setTokenName('') + } else { + setTokenName(token) + } + }} + /> + + ({ + value: model, + label: model + })) || []) + ]} + onchange={(model: string) => { + if (model === 'all') { + setModel('') + } else { + setModel(model) + } + }} + /> + + + + {[ + { label: t('dataDashboard.day'), value: 'day' }, + { label: t('dataDashboard.week'), value: 'week' }, + { label: t('dataDashboard.twoWeek'), value: 'two_week' }, + { label: t('dataDashboard.month'), value: 'month' } + ].map((item) => ( + + ))} + + + {/* -- header end */} + + + {/* chart 1 */} + + + + + + + + + + + + + {t('dataDashboard.callCount')} + + + {dashboardData?.total_count + ? dashboardData.total_count >= 10000 + ? `${Number((dashboardData.total_count / 10000).toFixed(3))}W` + : dashboardData.total_count.toLocaleString() + : 0} + + + + + + + + + + + + + {t('dataDashboard.exceptionCount')} + + + {dashboardData?.exception_count || 0} + + + + + + + + + + + + + {t('dataDashboard.rpm')} + + + {dashboardData?.rpm || 0} + + + + + + + + + + + + + + {t('dataDashboard.tpm')} + + + {dashboardData?.tpm || 0} + + + + + + + + + + + + + + + {t('dataDashboard.cost')} + + {currencySymbol === 'shellCoin' ? ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : currencySymbol === 'cny' ? ( + + ¥ + + ) : ( + + $ + + )} + + + {dashboardData?.used_amount ? Number(dashboardData.used_amount.toFixed(2)) : 0} + + + + + {/* chart 1 end */} + + + ) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/key/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/key/page.tsx new file mode 100644 index 000000000000..882c5a6f5cf7 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/key/page.tsx @@ -0,0 +1,184 @@ +'use client' + +import { Button, Flex, Link, Text } from '@chakra-ui/react' + +import KeyList from '@/components/user/KeyList' +import { useBackendStore } from '@/store/backend' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useSessionStore } from '@/store/session' +import { MyTooltip } from '@/components/common/MyTooltip' +export default function Key(): JSX.Element { + const { isInvitationActive } = useBackendStore() + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { session } = useSessionStore.getState() + const { invitationUrl } = useBackendStore() + let userInvitationUrl = '' + if (isInvitationActive && invitationUrl) { + const userId = session?.user.id + const baseUrl = new URL(invitationUrl).origin + userInvitationUrl = `${baseUrl}/?uid=${userId}` + } + + return ( + + {isInvitationActive ? ( + + + + + + + 🎉{t('keyList.invitationText1')} + + + + + {t('keyList.invitationText2')} + + + {t('keyList.invitationText3')} + + + + + {t('keyList.invitationText4')} + + + { + navigator.clipboard.writeText(userInvitationUrl) + }}> + {userInvitationUrl} + + + + + + + + + + + + + + ) : ( + + + + )} + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index 62d16d0f4bc6..658bcb8f2802 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -1,115 +1,15 @@ -'use client' import { Box, Flex } from '@chakra-ui/react' import SideBar from '@/components/user/Sidebar' -import { EVENT_NAME } from 'sealos-desktop-sdk' -import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' -import { useCallback, useEffect } from 'react' -import { initAppConfig } from '@/api/platform' -import { useI18n } from '@/providers/i18n/i18nContext' -import { useBackendStore } from '@/store/backend' -import { useTranslationClientSide } from '@/app/i18n/client' -import { usePathname } from 'next/navigation' -import { useRouter } from 'next/navigation' - export default function UserLayout({ children }: { children: React.ReactNode }) { - const router = useRouter() - const pathname = usePathname() - const { lng } = useI18n() - const { i18n } = useTranslationClientSide(lng) - const { setAiproxyBackend, setCurrencySymbol } = useBackendStore() - - const handleI18nChange = useCallback( - (data: { currentLanguage: string }) => { - const currentLng = i18n.resolvedLanguage // get the latest resolvedLanguage - const newLng = data.currentLanguage - - if (currentLng !== newLng) { - const currentPath = window.location.pathname - const pathWithoutLang = currentPath.split('/').slice(2).join('/') - router.push(`/${newLng}/${pathWithoutLang}`) - } - }, - [i18n.resolvedLanguage] - ) - - // init session - useEffect(() => { - const cleanup = createSealosApp() - ;(async () => { - try { - const newSession = JSON.stringify(await sealosApp.getSession()) - const oldSession = localStorage.getItem('session') - if (newSession && newSession !== oldSession) { - localStorage.setItem('session', newSession) - window.location.reload() - } - console.log('aiproxy: app init success') - } catch (err) { - console.log('aiproxy: app is not running in desktop') - if (!process.env.NEXT_PUBLIC_MOCK_USER) { - localStorage.removeItem('session') - } - } - })() - return () => { - if (cleanup && typeof cleanup === 'function') { - cleanup() - } - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - // init config and language - useEffect(() => { - const initConfig = async () => { - const { aiproxyBackend, currencySymbol } = await initAppConfig() - setAiproxyBackend(aiproxyBackend) - setCurrencySymbol(currencySymbol) - } - - initConfig() - - const initLanguage = async () => { - const pathLng = pathname.split('/')[1] - try { - const lang = await sealosApp.getLanguage() - if (pathLng !== lang.lng) { - const pathParts = pathname.split('/') - pathParts[1] = lang.lng - router.push(pathParts.join('/')) - router.refresh() - } - } catch (error) { - if (error instanceof Error) { - console.debug('Language initialization error:', error.message) - } else { - console.debug('Unknown language initialization error:', error) - } - } - } - - initLanguage() - - const cleanup = sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, handleI18nChange) - - return () => { - if (cleanup && typeof cleanup === 'function') { - cleanup() - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - return ( - - + + {/* Main Content */} - + {children} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/log/components/LogDetailModal.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/log/components/LogDetailModal.tsx new file mode 100644 index 000000000000..0268dd7ef626 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/log/components/LogDetailModal.tsx @@ -0,0 +1,634 @@ +'use client' + +import { + Box, + Flex, + Text, + Modal, + ModalOverlay, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalContent, + Grid, + Center, + Spinner +} from '@chakra-ui/react' +import { CurrencySymbol } from '@sealos/ui' +import { MyTooltip } from '@/components/common/MyTooltip' + +import { getUserLogDetail } from '@/api/platform' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { LogItem } from '@/types/user/logs' +import { useQuery } from '@tanstack/react-query' +import { QueryKey } from '@/types/query-key' +import { useBackendStore } from '@/store/backend' +import { getTranslationWithFallback } from '@/utils/common' +import ReactJson, { OnCopyProps } from 'react-json-view' +import { getTimeDiff } from '../tools/handleTime' +import { useMessage } from '@sealos/ui' + +export default function LogDetailModal({ + isOpen, + onClose, + rowData +}: { + isOpen: boolean + onClose: () => void + rowData: LogItem | null +}): React.JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { currencySymbol } = useBackendStore() + + const { data: logDetail, isLoading } = useQuery({ + queryKey: [QueryKey.GetUserLogDetail, rowData?.request_detail?.log_id], + queryFn: () => { + if (!rowData?.request_detail?.log_id) throw new Error('No log ID') + return getUserLogDetail(rowData.request_detail.log_id) + }, + enabled: !!rowData?.request_detail?.log_id + }) + + const isDetailLoading = !!rowData?.request_detail?.log_id && isLoading + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + // 定义默认的网格配置 + const gridConfig = { + labelWidth: '153px', + rowHeight: '48px', + jsonContentHeight: '122px' + } + + const renderDetailRow = ( + leftLabel: string | React.ReactNode, + leftValue: string | number | React.ReactNode | undefined, + rightLabel?: string | React.ReactNode, + rightValue?: string | number | React.ReactNode | undefined, + options?: { + labelWidth?: string + rowHeight?: string + isFirst?: boolean + isLast?: boolean + } + ) => { + // 辅助函数:渲染标签 + const renderLabel = (label: string | React.ReactNode) => { + if (typeof label === 'string') { + return ( + + {label} + + ) + } + return label + } + + // 辅助函数:渲染值 + const renderValue = (value: string | number | React.ReactNode | undefined) => { + if (typeof value === 'string' || typeof value === 'number') { + return ( + + {value} + + ) + } + return value + } + return ( + + + + {renderLabel(leftLabel)} + + + {renderValue(leftValue)} + + + {rightLabel && ( + + + {renderLabel(rightLabel)} + + + {renderValue(rightValue)} + + + )} + + ) + } + + const renderJsonContent = ( + label: string, + content: string | undefined, + options?: { + labelWidth?: string + contentHeight?: string + isFirst?: boolean + isLast?: boolean + } + ) => { + if (!content) return null + const handleCopy = (copy: OnCopyProps) => { + if (typeof window === 'undefined') return + + const copyText = + typeof copy.src === 'object' ? JSON.stringify(copy.src, null, 2) : String(copy.src) + + navigator.clipboard.writeText(copyText) + } + + let parsed = null + + try { + parsed = JSON.parse(content) + } catch { + parsed = content + } + return ( + + + + {label} + + + + {typeof parsed === 'object' ? ( + + ) : ( + + {parsed} + + )} + + + ) + } + + return isDetailLoading ? ( + + + + + + + {t('logs.logDetail')} + + + + + +
+ +
+
+
+
+ ) : ( + + + + + + + {t('logs.logDetail')} + + + + + + + {renderDetailRow( + t('logs.requestId'), + rowData?.request_id, + t('logs.status'), + + {rowData?.code === 200 + ? t('logs.success') + : `${t('logs.failed')} (${rowData?.code})`} + , + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: true + } + )} + {renderDetailRow( + 'Endpoint', + rowData?.endpoint, + t('logs.mode'), + getTranslationWithFallback( + `modeType.${String(rowData?.mode)}`, + 'modeType.0', + t as any + ), + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: false + } + )} + {renderDetailRow( + t('logs.requestTime'), + new Date(rowData?.created_at || 0).toLocaleString(), + t('logs.totalTime'), + getTimeDiff(rowData?.created_at || 0, rowData?.request_at || 0), + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: false + } + )} + {renderDetailRow( + t('logs.tokenName'), + rowData?.token_name, + t('logs.tokenId'), + rowData?.token_id, + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: false + } + )} + {renderDetailRow(t('logs.model'), rowData?.model, undefined, undefined, { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: false + })} + + {rowData?.content && + renderDetailRow( + t('logs.info'), + + { + navigator.clipboard.writeText(rowData.content || '').then( + () => { + message({ + status: 'success', + title: t('copySuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + (err) => { + message({ + status: 'warning', + title: t('copyFailed'), + description: err?.message || t('copyFailed'), + isClosable: true, + position: 'top' + }) + } + ) + }}> + {rowData.content} + + , + undefined, + undefined, + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: false + } + )} + + {logDetail?.request_body && + renderJsonContent(t('logs.requestBody'), logDetail.request_body, { + labelWidth: gridConfig.labelWidth, + contentHeight: gridConfig.jsonContentHeight, + isFirst: false + })} + {logDetail?.response_body && + renderJsonContent(t('logs.responseBody'), logDetail.response_body, { + labelWidth: gridConfig.labelWidth, + contentHeight: gridConfig.jsonContentHeight, + isLast: false + })} + + {renderDetailRow( + + + {t('key.inputPrice')} + + + + /{t('price.per1kTokens').toLowerCase()} + + , + rowData?.price, + + + {t('key.outputPrice')} + + + + /{t('price.per1kTokens').toLowerCase()} + + , + rowData?.completion_price, + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: false + } + )} + {renderDetailRow( + t('logs.inputTokens'), + rowData?.prompt_tokens, + t('logs.outputTokens'), + rowData?.completion_tokens, + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isFirst: false + } + )} + + {renderDetailRow( + + + + {t('logs.total_price')} + + + + , + rowData?.used_amount || 0, + undefined, + undefined, + { + labelWidth: gridConfig.labelWidth, + rowHeight: gridConfig.rowHeight, + isLast: true + } + )} + + + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/log/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/log/page.tsx new file mode 100644 index 000000000000..737671a09c19 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/log/page.tsx @@ -0,0 +1,662 @@ +'use client' + +import { + Box, + Flex, + Text, + Button, + Icon, + useDisclosure, + InputGroup, + InputRightElement, + Input +} from '@chakra-ui/react' +import { CurrencySymbol, MySelect, MyTooltip } from '@sealos/ui' +import { useMemo, useState } from 'react' + +import { getTokens, getUserLogs, getEnabledMode } from '@/api/platform' +import { useTranslationClientSide } from '@/app/i18n/client' +import SelectDateRange from '@/components/common/SelectDateRange' +import SwitchPage from '@/components/common/SwitchPage' +import { BaseTable } from '@/components/table/BaseTable' +import { useI18n } from '@/providers/i18n/i18nContext' +import { LogItem } from '@/types/user/logs' +import { useQuery } from '@tanstack/react-query' +import { getCoreRowModel, useReactTable, createColumnHelper } from '@tanstack/react-table' +import { QueryKey } from '@/types/query-key' +import { useBackendStore } from '@/store/backend' +import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle' +import { getTimeDiff } from './tools/handleTime' +import dynamic from 'next/dynamic' +import { useDebounce } from '@/hooks/useDebounce' + +const LogDetailModal = dynamic( + () => import('./components/LogDetailModal'), + { ssr: false } // 禁用服务端渲染 +) + +export default function Logs(): React.JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { currencySymbol } = useBackendStore() + const { isOpen, onOpen, onClose } = useDisclosure() + const [selectedRow, setSelectedRow] = useState(null) + + const [startTime, setStartTime] = useState(() => { + const currentDate = new Date() + currentDate.setDate(currentDate.getDate() - 3) + return currentDate + }) + const [endTime, setEndTime] = useState(new Date()) + const [keyName, setKeyName] = useState('') + const [codeType, setCodeType] = useState('all') + const [modelName, setModelName] = useState('') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [inputKeyword, setInputKeyword] = useState('') + const debouncedKeyword = useDebounce(inputKeyword, 500) // 500ms 延迟 0.5s + + const { data: logData, isLoading } = useQuery( + [ + QueryKey.GetUserLogs, + page, + pageSize, + keyName, + modelName, + startTime, + endTime, + codeType, + debouncedKeyword + ], + () => + getUserLogs({ + page, + perPage: pageSize, + token_name: keyName, + model_name: modelName, + keyword: debouncedKeyword, + code_type: codeType as 'all' | 'success' | 'error', + start_timestamp: startTime.getTime().toString(), + end_timestamp: endTime.getTime().toString() + }) + ) + + const columnHelper = createColumnHelper() + + const columns = useMemo( + () => [ + columnHelper.accessor('token_name', { + header: () => ( + + {t('logs.name')} + + ), + id: 'token_name' + }), + columnHelper.accessor('model', { + header: () => ( + + {t('logs.model')} + + ), + id: 'model' + }), + columnHelper.accessor('prompt_tokens', { + header: () => ( + + {t('logs.prompt_tokens')} + + ), + id: 'prompt_tokens' + }), + columnHelper.accessor('completion_tokens', { + header: () => ( + + {t('logs.completion_tokens')} + + ), + id: 'completion_tokens' + }), + + columnHelper.display({ + header: () => ( + + {t('logs.totalTime')} + + ), + cell: ({ row }) => ( + + {getTimeDiff(row.original.created_at, row.original.request_at)} + + ), + id: 'total_time' + }), + + columnHelper.accessor('code', { + header: () => ( + + {t('logs.status')} + + ), + cell: ({ getValue, row }) => { + const code = getValue() + return ( + + + {code !== 200 ? `${t('logs.failed')} (${row.original.code})` : code} + + {code !== 200 && ( + + + + + + )} + + ) + }, + id: 'status' + }), + + columnHelper.accessor('created_at', { + header: () => ( + + {t('logs.time')} + + ), + cell: ({ row }) => new Date(row.original.created_at).toLocaleString(), + id: 'created_at' + }), + columnHelper.accessor('used_amount', { + header: () => { + return ( + + + + + {t('logs.total_price')} + + + + + + ) + }, + id: 'used_amount' + }), + + columnHelper.display({ + header: () => ( + + {t('logs.actions')} + + ), + cell: ({ row }) => ( + + ), + id: 'detail' + }) + ], + [t, currencySymbol] + ) + + const table = useReactTable({ + data: logData?.logs || [], + columns, + getCoreRowModel: getCoreRowModel() + }) + + return ( + + + {/* -- header */} + + + + {t('logs.call_log')} + + + + { + setInputKeyword(e.target.value) + }} + /> + + + + + + + + + + + + + {/* -- the first row */} + + + + {t('logs.name')} + + + dropdownItems={['all', ...(logData?.token_names || [])]} + setSelectedItem={(value) => { + if (value === 'all') { + setKeyName('') + } else { + setKeyName(value) + } + }} + handleDropdownItemFilter={(dropdownItems, inputValue) => { + const lowerCasedInput = inputValue.toLowerCase() + return dropdownItems.filter( + (item) => !inputValue || item.toLowerCase().includes(lowerCasedInput) + ) + }} + handleDropdownItemDisplay={(dropdownItem) => { + return ( + + {dropdownItem} + + ) + }} + flexProps={{ w: '280px' }} + placeholder={t('logs.select_token_name')} + /> + + + + + {t('logs.modal')} + + + dropdownItems={['all', ...(logData?.models || [])]} + setSelectedItem={(value) => { + if (value === 'all') { + setModelName('') + } else { + setModelName(value) + } + }} + handleDropdownItemFilter={(dropdownItems, inputValue) => { + const lowerCasedInput = inputValue.toLowerCase() + return dropdownItems.filter( + (item) => !inputValue || item.toLowerCase().includes(lowerCasedInput) + ) + }} + handleDropdownItemDisplay={(dropdownItem) => { + return ( + + {dropdownItem} + + ) + }} + flexProps={{ w: '280px' }} + placeholder={t('logs.select_modal')} + /> + + + + + {t('logs.status')} + + { + setCodeType(val) + }} + /> + + + + + {t('logs.time')} + + + + + {/* -- the first row end */} + + + {/* -- header end */} + + {/* -- table */} + + + setPage(idx)} + /> + + {/* -- table end */} + + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/log/tools/handleTime.ts b/frontend/providers/aiproxy/app/[lng]/(user)/log/tools/handleTime.ts new file mode 100644 index 000000000000..0b892c563f3d --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/log/tools/handleTime.ts @@ -0,0 +1,4 @@ +export const getTimeDiff = (createdAt: number, requestAt: number) => { + const diff = Number(((createdAt - requestAt) / 1000).toFixed(4)).toString() + return `${diff}s` +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx deleted file mode 100644 index 7cbb2ef35adc..000000000000 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ /dev/null @@ -1,260 +0,0 @@ -'use client' - -import { Box, Flex, Text, Button, Icon } from '@chakra-ui/react' -import { CurrencySymbol, MySelect, MyTooltip } from '@sealos/ui' -import { useMemo, useState } from 'react' - -import { getKeys, getLogs, getModels } from '@/api/platform' -import { useTranslationClientSide } from '@/app/i18n/client' -import SelectDateRange from '@/components/SelectDateRange' -import SwitchPage from '@/components/SwitchPage' -import { BaseTable } from '@/components/table/baseTable' -import { useI18n } from '@/providers/i18n/i18nContext' -import { LogItem } from '@/types/log' -import { useQuery } from '@tanstack/react-query' -import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table' -import { useBackendStore } from '@/store/backend' - -const mockStatus = ['all', 'success', 'failed'] - -export default function Home(): React.JSX.Element { - const { lng } = useI18n() - const { t } = useTranslationClientSide(lng, 'common') - - const [startTime, setStartTime] = useState(() => { - const currentDate = new Date() - currentDate.setMonth(currentDate.getMonth() - 1) - return currentDate - }) - const [endTime, setEndTime] = useState(new Date()) - const [name, setName] = useState('') - const [modelName, setModelName] = useState('') - const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(10) - const [logData, setLogData] = useState([]) - const [total, setTotal] = useState(0) - const { currencySymbol } = useBackendStore() - - const { data: models = [] } = useQuery(['getModels'], () => getModels()) - const { data: tokenData } = useQuery(['getKeys'], () => getKeys({ page: 1, perPage: 100 })) - - const { isLoading } = useQuery( - ['getLogs', page, pageSize, name, modelName, startTime, endTime], - () => - getLogs({ - page, - perPage: pageSize, - token_name: name, - model_name: modelName, - start_timestamp: startTime.getTime().toString(), - end_timestamp: endTime.getTime().toString() - }), - { - onSuccess: (data) => { - if (!data.logs) { - setLogData([]) - setTotal(0) - return - } - setLogData(data.logs) - setTotal(data.total) - } - } - ) - - const columns = useMemo[]>(() => { - return [ - { - header: t('logs.name'), - accessorKey: 'token_name' - }, - { - header: t('logs.model'), - accessorKey: 'model' - }, - { - header: t('logs.prompt_tokens'), - accessorKey: 'prompt_tokens' - }, - { - header: t('logs.completion_tokens'), - accessorKey: 'completion_tokens' - }, - - { - header: t('logs.status'), - accessorFn: (row) => (row.code === 200 ? t('logs.success') : t('logs.failed')), - cell: ({ getValue }) => { - const value = getValue() as string - return ( - - {value} - - ) - }, - id: 'status' - }, - { - header: t('logs.time'), - accessorFn: (row) => new Date(row.created_at).toLocaleString(), - id: 'created_at' - }, - { - accessorKey: 'used_amount', - id: 'used_amount', - header: () => { - return ( - - - - {t('logs.total_price')} - - - - - ) - } - } - ] - }, []) - - const table = useReactTable({ - data: logData, - columns, - getCoreRowModel: getCoreRowModel() - }) - - return ( - - - - {t('logs.call_log')} - - - - - - - - {t('logs.name')} - - ({ - value: item.name, - label: item.name - })) || []) - ]} - onchange={(val: string) => { - if (val === 'all') { - setName('') - } else { - setName(val) - } - }} - /> - - - - - {t('logs.modal')} - - ({ - value: item, - label: item - })) || [] - } - onchange={(val: string) => { - if (val === 'all') { - setModelName('') - } else { - setModelName(val) - } - }} - /> - - - - - - {t('logs.time')} - - - - - - - setPage(idx)} - /> - - - ) -} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx index b4e5aa098b61..fd10b773076a 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx @@ -11,14 +11,19 @@ import { Thead, Tr, Center, - Spinner + Spinner, + Button, + Icon, + Input, + InputGroup, + InputRightElement, + Badge } from '@chakra-ui/react' import { useTranslationClientSide } from '@/app/i18n/client' import { useI18n } from '@/providers/i18n/i18nContext' -import { useQuery } from '@tanstack/react-query' -import { ModelPrice } from '@/types/backend' -import { getModelPrices } from '@/api/platform' -import { useMemo } from 'react' +import { useQuery, UseQueryResult } from '@tanstack/react-query' +import { getEnabledMode } from '@/api/platform' +import { useMemo, useState, useEffect } from 'react' import { createColumnHelper, getCoreRowModel, @@ -26,46 +31,400 @@ import { flexRender } from '@tanstack/react-table' import { CurrencySymbol } from '@sealos/ui' -import { ModelIdentifier } from '@/types/front' -import { MyTooltip } from '@/components/MyTooltip' +import { MyTooltip } from '@/components/common/MyTooltip' import { useMessage } from '@sealos/ui' -// icons -import OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' -import QwenIcon from '@/ui/svg/icons/modelist/qianwen.svg' -import ChatglmIcon from '@/ui/svg/icons/modelist/chatglm.svg' -import DeepseekIcon from '@/ui/svg/icons/modelist/deepseek.svg' -import MoonshotIcon from '@/ui/svg/icons/modelist/moonshot.svg' -import SparkdeskIcon from '@/ui/svg/icons/modelist/sparkdesk.svg' -import AbabIcon from '@/ui/svg/icons/modelist/minimax.svg' -import DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg' -import ErnieIcon from '@/ui/svg/icons/modelist/ernie.svg' +import { ModelConfig } from '@/types/models/model' import Image, { StaticImageData } from 'next/image' +import { QueryKey } from '@/types/query-key' +import { getTranslationWithFallback } from '@/utils/common' import { useBackendStore } from '@/store/backend' +import { modelIcons } from '@/ui/icons/mode-icons' +import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle' +import { useDebounce } from '@/hooks/useDebounce' -function Price() { +type SortDirection = 'asc' | 'desc' | false + +const getModelIcon = (modelOwner: string): StaticImageData => { + const icon = modelIcons[modelOwner as keyof typeof modelIcons] || modelIcons['default'] + return icon +} + +// 在组件外部定义样式配置 +const MODEL_TYPE_STYLES = { + 1: { + background: '#F0FBFF', + color: '#0884DD' + }, + 2: { + background: '#F4F4F7', + color: '#383F50' + }, + 3: { + background: '#EBFAF8', + color: '#007E7C' + }, + 4: { + background: '#FEF3F2', + color: '#F04438' + }, + 5: { + background: '#F0EEFF', + color: '#6F5DD7' + }, + 6: { + background: '#FFFAEB', + color: '#DC6803' + }, + 7: { + background: '#FAF1FF', + color: '#9E53C1' + }, + 8: { + background: '#FFF1F6', + color: '#E82F72' + }, + 9: { + background: '#F0F4FF', + color: '#3370FF' + }, + 10: { + background: '#EDFAFF', + color: '#0077A9' + }, + default: { + background: '#F4F4F7', + color: '#383F50' + } +} as const + +// 在组件中使用 +const getTypeStyle = (type: number) => { + return MODEL_TYPE_STYLES[type as keyof typeof MODEL_TYPE_STYLES] || MODEL_TYPE_STYLES.default +} + +export default function Price() { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') + + const [modelOwner, setModelOwner] = useState('') + const [modelType, setModelType] = useState('') + + const [searchInput, setSearchInput] = useState('') + const debouncedSearch = useDebounce(searchInput, 500) + + interface FilterParams { + owner: string + type: string + name: string + } + + const filterModels = (modelConfigs: ModelConfig[], filterParams: FilterParams): ModelConfig[] => { + return modelConfigs.filter((config) => { + const ownerMatch = + !filterParams.owner || + filterParams.owner === t('price.all') || + getTranslationWithFallback( + `modeOwner.${String(config.owner)}`, + 'modeOwner.unknown', + t as any + ) === filterParams.owner + + const typeMatch = + !filterParams.type || + filterParams.type === t('price.all') || + getTranslationWithFallback(`modeType.${String(config.type)}`, 'modeType.0', t as any) === + filterParams.type + + const nameMatch = + !filterParams.name || config.model.toLowerCase().includes(filterParams.name.toLowerCase()) + + return ownerMatch && typeMatch && nameMatch + }) + } + + const { + isLoading, + data: modelConfigs = [] as ModelConfig[], + refetch + }: UseQueryResult = useQuery([QueryKey.GetEnabledModels], () => getEnabledMode()) + + const filteredModelConfigs = useMemo(() => { + return filterModels(modelConfigs, { + owner: modelOwner, + type: modelType, + name: debouncedSearch + }) + }, [modelConfigs, modelOwner, modelType, debouncedSearch]) + return ( - - - - - {t('price.title')} - + + + + + {/* row 1 */} + + + {t('price.title')} + + + + {/* row 1 end */} + + {/* row 2 */} + + + + + {t('price.modelOwner')} + + + dropdownItems={[ + { icon: '', name: t('price.all') }, + ...Array.from( + new Map( + modelConfigs.map((config) => [ + config.owner, + { + icon: config.owner, + name: getTranslationWithFallback( + `modeOwner.${String(config.owner)}`, + 'modeOwner.unknown', + t as any + ) + } + ]) + ).values() + ) + ]} + setSelectedItem={(modelOwner) => { + setModelOwner(modelOwner.name) + }} + handleDropdownItemFilter={(dropdownItems, inputValue) => { + const lowerCasedInput = inputValue.toLowerCase() + return dropdownItems.filter( + (item) => !inputValue || item.name.toLowerCase().includes(lowerCasedInput) + ) + }} + handleDropdownItemDisplay={(dropdownItem) => { + const iconSrc = getModelIcon(dropdownItem.icon) + if (dropdownItem.name === t('price.all')) { + return ( + + default + + {dropdownItem.name} + + + ) + } + return ( + + {dropdownItem.icon} + + {dropdownItem.name} + + + ) + }} + flexProps={{ w: '240px' }} + initSelectedItem={{ icon: '', name: t('price.all') }} + handleInputDisplay={(dropdownItem) => dropdownItem.name} + /> + + + + {t('price.modelType')} + + + dropdownItems={[ + t('price.all'), + ...new Set( + modelConfigs.map((config) => + getTranslationWithFallback( + `modeType.${String(config.type)}`, + 'modeType.0', + t as any + ) + ) + ) + ]} + setSelectedItem={(modelType) => { + setModelType(modelType) + }} + handleDropdownItemFilter={(dropdownItems, inputValue) => { + const lowerCasedInput = inputValue.toLowerCase() + return dropdownItems.filter( + (item) => !inputValue || item.toLowerCase().includes(lowerCasedInput) + ) + }} + handleDropdownItemDisplay={(dropdownItem) => { + return ( + + {dropdownItem} + + ) + }} + flexProps={{ w: '240px' }} + initSelectedItem={t('price.all')} + /> + + + + + + { + setSearchInput(e.target.value) + }} + /> + + + + + + + + + + + - + {isLoading ? ( + + ) : ( + + )} @@ -73,142 +432,235 @@ function Price() { ) } -function PriceTable() { +const ModelComponent = ({ modelConfig }: { modelConfig: ModelConfig }) => { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') - const { isLoading, data } = useQuery({ - queryKey: ['getModelPrices'], - queryFn: () => getModelPrices(), - refetchOnReconnect: true + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' }) - const { currencySymbol } = useBackendStore() - const modelGroups = { - ernie: { - icon: ErnieIcon, - identifiers: ['ernie'] - }, - qwen: { - icon: QwenIcon, - identifiers: ['qwen'] - }, - chatglm: { - icon: ChatglmIcon, - identifiers: ['chatglm', 'glm'] - }, - deepseek: { - icon: DeepseekIcon, - identifiers: ['deepseek'] - }, - moonshot: { - icon: MoonshotIcon, - identifiers: ['moonshot'] - }, - sparkdesk: { - icon: SparkdeskIcon, - identifiers: ['sparkdesk'] - }, - abab: { - icon: AbabIcon, - identifiers: ['abab'] - }, - doubao: { - icon: DoubaoIcon, - identifiers: ['doubao'] - } - } + const iconSrc = getModelIcon(modelConfig.owner) - const getIdentifier = (modelName: string): ModelIdentifier => { - return modelName.toLowerCase().split(/[-._\d]/)[0] as ModelIdentifier - } - - const getModelIcon = (modelName: string): StaticImageData => { - const identifier = getIdentifier(modelName) - const group = Object.values(modelGroups).find((group) => group.identifiers.includes(identifier)) - return group?.icon || OpenAIIcon - } - - const ModelComponent = ({ modelName }: { modelName: string }) => { - const { message } = useMessage({ - warningBoxBg: 'var(--Yellow-50, #FFFAEB)', - warningIconBg: 'var(--Yellow-500, #F79009)', - warningIconFill: 'white', - successBoxBg: 'var(--Green-50, #EDFBF3)', - successIconBg: 'var(--Green-600, #039855)', - successIconFill: 'white' - }) - const iconSrc = getModelIcon(modelName) - - return ( - - {modelName} - - - navigator.clipboard.writeText(modelName).then( - () => { - message({ - status: 'success', - title: t('copySuccess'), - isClosable: true, - duration: 2000, - position: 'top' - }) - }, - (err) => { - message({ - status: 'warning', - title: t('copyFailed'), - description: err?.message || t('copyFailed'), - isClosable: true, - position: 'top' - }) - } - ) - } - cursor="pointer"> - {modelName} - - + return ( + + + {modelConfig.model} + + + + navigator.clipboard.writeText(modelConfig.model).then( + () => { + message({ + status: 'success', + title: t('copySuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + (err) => { + message({ + status: 'warning', + title: t('copyFailed'), + description: err?.message || t('copyFailed'), + isClosable: true, + position: 'top' + }) + } + ) + } + cursor="pointer"> + {modelConfig.model} + + + {modelConfig.config?.vision && ( + + {t('price.modelVisionLabel')} + + } + width="auto" + height="auto"> + + + + + + {t('price.modelVision')} + + + + )} + {modelConfig.config?.tool_choice && ( + + + + + + + {t('price.modelToolChoice')} + + + )} + {modelConfig.config?.max_context_tokens && ( + + + {`${ + modelConfig.config.max_context_tokens % 1024 === 0 + ? Math.ceil(modelConfig.config.max_context_tokens / 1024) + : Math.ceil(modelConfig.config.max_context_tokens / 1000) + }K`} + + + )} + {modelConfig.config?.max_output_tokens && ( + + + {`${Math.ceil(modelConfig.config.max_output_tokens / 1024)}K ${t( + 'price.response' + )}`} + + + )} + - ) - } - - const sortModelsByIdentifier = (models: ModelPrice[]): ModelPrice[] => { - const groupedModels = new Map() + + ) +} - // Group models by identifier - models.forEach((model) => { - const identifier = getIdentifier(model.name) - if (!groupedModels.has(identifier)) { - groupedModels.set(identifier, []) - } - groupedModels.get(identifier)!.push(model) - }) +function PriceTable({ + modelConfigs, + isLoading +}: { + modelConfigs: ModelConfig[] + isLoading: boolean +}) { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') - // Define order based on modelGroups - const orderMap = new Map(Object.keys(modelGroups).map((key, index) => [key, index])) + const { currencySymbol } = useBackendStore() - // Sort based on modelGroups order, unknown models go to the end - const sortedEntries = Array.from(groupedModels.entries()).sort((a, b) => { - const orderA = orderMap.has(a[0]) ? orderMap.get(a[0])! : Number.MAX_VALUE - const orderB = orderMap.has(b[0]) ? orderMap.get(b[0])! : Number.MAX_VALUE - return orderA - orderB - }) + const [sortConfig, setSortConfig] = useState({ + column: '', + direction: false as SortDirection + }) - return sortedEntries.flatMap(([_, models]) => models) + // 处理排序 + const handleSort = (column: string, direction: SortDirection) => { + // 如果点击相同的列并且方向相同,则取消排序 + if (sortConfig.column === column && sortConfig.direction === direction) { + setSortConfig({ column: '', direction: false }) + return + } + setSortConfig({ column, direction }) } - const columnHelper = createColumnHelper() + const columnHelper = createColumnHelper() const columns = [ - columnHelper.accessor((row) => row.name, { - id: 'name', + columnHelper.accessor((row) => row.model, { + id: 'model', header: () => ( ), - cell: (info) => + cell: (info) => + }), + columnHelper.accessor((row) => row.type, { + id: 'type', + header: () => ( + + {t('key.modelType')} + + ), + cell: (info) => ( + + + {getTranslationWithFallback( + `modeType.${String(info.getValue())}`, + 'modeType.0', + t as any + )} + + + ) + }), + columnHelper.accessor((row) => row.rpm, { + id: 'rpm', + header: () => ( + + + {t('price.modelRpm')} + + + {t('price.modelRpmTooltip')} + + }> + + + + + + ), + cell: (info) => ( + + {info.getValue()} + + ) }), - columnHelper.accessor((row) => row.prompt, { - id: 'inputPrice', + columnHelper.accessor((row) => row.input_price, { + id: 'input_price', header: () => { return ( - - + + - + + + + {t('price.sortUpTooltip')} + + }> + handleSort('input_price', 'asc')} + cursor="pointer" + _hover={{ opacity: 0.8 }}> + + + + + + + {t('price.sortDownTooltip')} + + }> + handleSort('input_price', 'desc')} + cursor="pointer" + _hover={{ opacity: 0.8 }}> + + + + + + + ) }, cell: (info) => ( @@ -265,11 +890,11 @@ function PriceTable() { ) }), - columnHelper.accessor((row) => row.completion, { - id: 'outputPrice', + columnHelper.accessor((row) => row.output_price, { + id: 'output_price', header: () => ( - - + + - + + + {t('price.sortUpTooltip')} + + }> + handleSort('output_price', 'asc')} + cursor="pointer" + _hover={{ opacity: 0.8 }}> + + + + + + + {t('price.sortDownTooltip')} + + }> + handleSort('output_price', 'desc')} + cursor="pointer" + _hover={{ opacity: 0.8 }}> + + + + + + +
), cell: (info) => ( sortModelsByIdentifier(data || []), [data]) + const tableData = useMemo(() => { + if (!sortConfig.direction || !sortConfig.column) { + return modelConfigs + } + + return [...modelConfigs].sort((a, b) => { + let aValue = a[sortConfig.column as keyof ModelConfig] + let bValue = b[sortConfig.column as keyof ModelConfig] + + // 确保数值比较 + if (typeof aValue === 'string') aValue = parseFloat(aValue as string) || 0 + if (typeof bValue === 'string') bValue = parseFloat(bValue as string) || 0 + + if (sortConfig.direction === 'asc') { + return (aValue as number) - (bValue as number) + } else { + return (bValue as number) - (aValue as number) + } + }) + }, [modelConfigs, sortConfig]) const table = useReactTable({ - data: sortedData, + data: tableData, columns, getCoreRowModel: getCoreRowModel() }) - return isLoading ? ( -
- -
- ) : ( - + return ( + {table.getHeaderGroups().map((headerGroup) => ( @@ -341,22 +1059,35 @@ function PriceTable() { ))} - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} + {isLoading ? ( + + - ))} + ) : ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + )}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ +
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
) } - -export default Price diff --git a/frontend/providers/aiproxy/app/[lng]/globals.css b/frontend/providers/aiproxy/app/[lng]/globals.css index e6e10f493ddf..c356d55399f5 100644 --- a/frontend/providers/aiproxy/app/[lng]/globals.css +++ b/frontend/providers/aiproxy/app/[lng]/globals.css @@ -100,42 +100,39 @@ textarea::placeholder { height: 100%; } -::-webkit-scrollbar, ::-webkit-scrollbar { - width: 8px; - height: 8px; - border-radius: 8px; + width: 6px; + height: 6px; + border-radius: 6px; } -::-webkit-scrollbar-track, + ::-webkit-scrollbar-track { + margin: 24px 0; background: transparent !important; - border-radius: 2px; + border-radius: 6px; } -::-webkit-scrollbar-thumb, + ::-webkit-scrollbar-thumb { - background: rgba(189, 193, 197, 1) !important; - border-radius: 2px; -} -::-webkit-scrollbar-thumb:hover, -::-webkit-scrollbar-thumb:hover { - background: rgba(189, 193, 197, 1) !important; + min-height: 40px; + border-radius: 6px; + background: var(--Gray-Modern-200, #e8ebf0) !important; } div { - &::-webkit-scrollbar-thumb, &::-webkit-scrollbar-thumb { + min-height: 40px; background: transparent !important; - border-radius: 2px; - transition: 1s; + border-radius: 6px; + transition: background 0.3s ease; } + + &::-webkit-scrollbar-track { + margin: 24px 0; + } + &:hover { - &::-webkit-scrollbar-thumb, &::-webkit-scrollbar-thumb { - background: rgba(189, 193, 197, 0.5) !important; - } - &::-webkit-scrollbar-thumb:hover, - &::-webkit-scrollbar-thumb:hover { - background: rgba(189, 193, 197, 1) !important; + background: var(--Gray-Modern-200, #e8ebf0) !important; } } } diff --git a/frontend/providers/aiproxy/app/[lng]/layout.tsx b/frontend/providers/aiproxy/app/[lng]/layout.tsx index 5b861c6a6b42..72d516fc5c12 100644 --- a/frontend/providers/aiproxy/app/[lng]/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -5,7 +5,8 @@ import { useTranslationServerSide } from '@/app/i18n/server' import { fallbackLng, languages } from '@/app/i18n/settings' import ChakraProviders from '@/providers/chakra/providers' import { I18nProvider } from '@/providers/i18n/i18nContext' -import QueryProvider from '@/providers/chakra/QueryProvider' +import QueryProvider from '@/providers/tanstack-query/QueryProvider' +import InitializeApp from '@/components/InitializeApp' import './globals.css' import 'react-day-picker/dist/style.css' @@ -47,7 +48,10 @@ export default async function RootLayout({ - {children} + + + {children} + diff --git a/frontend/providers/aiproxy/app/api/admin/channel/[id]/route.ts b/frontend/providers/aiproxy/app/api/admin/channel/[id]/route.ts new file mode 100644 index 000000000000..5f30495b079c --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/channel/[id]/route.ts @@ -0,0 +1,151 @@ +import { NextRequest, NextResponse } from 'next/server' +import { ChannelInfo } from '@/types/admin/channels/channelInfo' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { CreateChannelRequest } from '@/types/admin/channels/channelInfo' + +export const dynamic = 'force-dynamic' + +export type GetChannelsResponse = ApiResp<{ + channels: ChannelInfo[] + total: number +}> + +async function updateChannel(channelData: CreateChannelRequest, id: string): Promise { + try { + const url = new URL( + `/api/channel/${id}`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify(channelData), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to create channel') + } + } catch (error) { + console.error('admin channels api: create channel error:## ', error) + throw error + } +} + +// update channel +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Channel id is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + const channelData: CreateChannelRequest = await request.json() + await updateChannel(channelData, params.id) + + return NextResponse.json({ + code: 200, + message: 'Channel created successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin channels api: create channel error:## ', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} + +async function deleteChannel(id: string): Promise { + try { + const url = new URL( + `/api/channel/${id}`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'admin channels api:ai proxy backend error') + } + } catch (error) { + console.error('admin channels api: delete channel error:', error) + throw error + } +} + +// delete channel +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Channel id is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + await deleteChannel(params.id) + return NextResponse.json({ + code: 200, + message: 'Channel deleted successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin channels api: delete channel error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/channel/[id]/status/route.ts b/frontend/providers/aiproxy/app/api/admin/channel/[id]/status/route.ts new file mode 100644 index 000000000000..55d47d688ecd --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/channel/[id]/status/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server' +import { ChannelInfo } from '@/types/admin/channels/channelInfo' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { CreateChannelRequest } from '@/types/admin/channels/channelInfo' + +export const dynamic = 'force-dynamic' + +// update channel status + +async function updateChannelStatus(id: string, status: number): Promise { + try { + const url = new URL( + `/api/channel/${id}/status`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify({ status }), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to update channel status') + } + } catch (error) { + console.error('admin channels api: update channel status error:## ', error) + throw error + } +} + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Channel id is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + const { status }: { status: number } = await request.json() + await updateChannelStatus(params.id, status) + + return NextResponse.json({ + code: 200, + message: 'Channel status updated successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin channels api: update channel status error:## ', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/channel/all/route.ts b/frontend/providers/aiproxy/app/api/admin/channel/all/route.ts new file mode 100644 index 000000000000..31fdf12323de --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/channel/all/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server' +import { ChannelInfo } from '@/types/admin/channels/channelInfo' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' + +export const dynamic = 'force-dynamic' + +type ApiProxyBackendAllChannelResponse = ApiProxyBackendResp + +export type GetAllChannelResponse = ApiResp + +async function fetchChannels(): Promise { + try { + const url = new URL( + `/api/channels/all`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + const result: ApiProxyBackendAllChannelResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'admin channels api:ai proxy backend error') + } + return result?.data || [] + } catch (error) { + console.error('admin channels api: fetch all channels from ai proxy backend error:', error) + throw error + } +} + +// get all channels +export async function GET(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + const channels = await fetchChannels() + return NextResponse.json({ + code: 200, + data: channels + } satisfies GetAllChannelResponse) + } catch (error) { + console.error('admin channels api: get all channels error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies GetAllChannelResponse, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/channel/route.ts b/frontend/providers/aiproxy/app/api/admin/channel/route.ts new file mode 100644 index 000000000000..58c73c638432 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/channel/route.ts @@ -0,0 +1,171 @@ +import { NextRequest, NextResponse } from 'next/server' +import { ChannelInfo } from '@/types/admin/channels/channelInfo' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { CreateChannelRequest } from '@/types/admin/channels/channelInfo' + +export const dynamic = 'force-dynamic' + +type ApiProxyBackendChannelsSearchResponse = ApiProxyBackendResp<{ + channels: ChannelInfo[] + total: number +}> + +export type ChannelQueryParams = { + page: number + perPage: number +} + +export type GetChannelsResponse = ApiResp<{ + channels: ChannelInfo[] + total: number +}> + +function validateParams(queryParams: ChannelQueryParams): string | null { + if (queryParams.page < 1) { + return 'Page number must be greater than 0' + } + if (queryParams.perPage < 1 || queryParams.perPage > 100) { + return 'Per page must be between 1 and 100' + } + return null +} + +async function fetchChannels( + queryParams: ChannelQueryParams +): Promise<{ channels: ChannelInfo[]; total: number }> { + try { + const url = new URL( + `/api/channels/search`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + url.searchParams.append('p', queryParams.page.toString()) + url.searchParams.append('per_page', queryParams.perPage.toString()) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + const result: ApiProxyBackendChannelsSearchResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'admin channels api:ai proxy backend error') + } + return { + channels: result?.data?.channels || [], + total: result?.data?.total || 0 + } + } catch (error) { + console.error('admin channels api: fetch channels from ai proxy backend error:', error) + throw error + } +} + +async function createChannel(channelData: CreateChannelRequest): Promise { + try { + const url = new URL( + `/api/channel/`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify(channelData), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to create channel') + } + } catch (error) { + console.error('admin channels api: create channel error:', error) + throw error + } +} + +// get channels +export async function GET(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + const searchParams = request.nextUrl.searchParams + + const queryParams: ChannelQueryParams = { + page: parseInt(searchParams.get('page') || '1', 10), + perPage: parseInt(searchParams.get('perPage') || '10', 10) + } + + const validationError = validateParams(queryParams) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + const { channels, total } = await fetchChannels(queryParams) + return NextResponse.json({ + code: 200, + data: { + channels: channels, + total: total + } + } satisfies GetChannelsResponse) + } catch (error) { + console.error('admin channels api: get channels error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies GetChannelsResponse, + { status: 500 } + ) + } +} + +// create channel +export async function POST(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + const channelData: CreateChannelRequest = await request.json() + await createChannel(channelData) + + return NextResponse.json({ + code: 200, + message: 'Channel created successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin channels api: create channel error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/channel/type-name/route.ts b/frontend/providers/aiproxy/app/api/admin/channel/type-name/route.ts new file mode 100644 index 000000000000..605ec2fb285d --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/channel/type-name/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server' +import { ChannelTypeMapName } from '@/types/admin/channels/channelInfo' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' + +export const dynamic = 'force-dynamic' + +type ApiProxyBackendChannelTypeMapNameResponse = ApiProxyBackendResp + +export type GetChannelTypeNamesResponse = ApiResp + +async function fetchChannelTypeNames(): Promise { + try { + const url = new URL( + `/api/channels/type_names`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + const result: ApiProxyBackendChannelTypeMapNameResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'admin channels api:ai proxy backend error') + } + return result.data + } catch (error) { + console.error( + 'admin channels api: fetch channel type names from ai proxy backend error:', + error + ) + throw error + } +} + +export async function GET( + request: NextRequest +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + const channelTypeNames = await fetchChannelTypeNames() + + return NextResponse.json({ + code: 200, + data: channelTypeNames + } satisfies GetChannelTypeNamesResponse) + } catch (error) { + console.error('admin channels api: get channel type names error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies GetChannelTypeNamesResponse, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/channel/upload/route.ts b/frontend/providers/aiproxy/app/api/admin/channel/upload/route.ts new file mode 100644 index 000000000000..6c2a7e0153dd --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/channel/upload/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { CreateChannelRequest } from '@/types/admin/channels/channelInfo' + +export const dynamic = 'force-dynamic' + +// 解析文件内容 +async function parseFormData(req: NextRequest): Promise { + try { + const formData = await req.formData() + const file = formData.get('file') + + if (!file || !(file instanceof File)) { + throw new Error('No file uploaded') + } + + // 读取文件内容 + const fileContent = await file.text() + const channelData = JSON.parse(fileContent) + + if (!Array.isArray(channelData)) { + throw new Error('Invalid file format: expected array of channel data') + } + + return channelData + } catch (error) { + throw error + } +} + +// 创建通道 +async function createChannels(channelData: CreateChannelRequest[]): Promise { + const url = new URL( + '/api/channels/', + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify(channelData), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to create channels') + } +} + +// 处理上传请求 +export async function POST(request: NextRequest): Promise> { + try { + // 验证管理员权限 + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + // 解析上传的文件 + const channelData = await parseFormData(request) + + // 创建通道 + await createChannels(channelData) + + return NextResponse.json({ + code: 200, + message: 'Channels created successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin channels api: create channels error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/group/[id]/qpm/route.ts b/frontend/providers/aiproxy/app/api/admin/group/[id]/qpm/route.ts new file mode 100644 index 000000000000..f5c3ce62e716 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/group/[id]/qpm/route.ts @@ -0,0 +1,78 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' + +export const dynamic = 'force-dynamic' + +async function updateGroupQpm(qpm: number, id: string): Promise { + try { + const url = new URL( + `/api/group/${id}/qpm`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify({ qpm: qpm }), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to update group qpm') + } + } catch (error) { + console.error('admin groups api: update group qpm error:## ', error) + throw error + } +} + +// update group qpm +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Group id is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + const { qpm }: { qpm: number } = await request.json() + await updateGroupQpm(qpm, params.id) + + return NextResponse.json({ + code: 200, + message: 'Group qpm updated successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin groups api: update group qpm error:## ', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/admin/group/[id]/route.ts similarity index 61% rename from frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts rename to frontend/providers/aiproxy/app/api/admin/group/[id]/route.ts index 26ed34f6ff20..d197311e8954 100644 --- a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts +++ b/frontend/providers/aiproxy/app/api/admin/group/[id]/route.ts @@ -1,18 +1,16 @@ +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { parseJwtToken } from '@/utils/backend/auth' +import { isAdmin } from '@/utils/backend/isAdmin' import { NextRequest, NextResponse } from 'next/server' -import { parseJwtToken } from '@/utils/auth' -export const dynamic = 'force-dynamic' -interface DeleteTokenResponse { - message: string - success: boolean -} - -async function deleteToken(group: string, id: string): Promise { +// delete +async function deleteGroup(id: string): Promise { try { const url = new URL( - `/api/token/${group}/${id}`, + `/api/group/${id}`, global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy ) + const token = global.AppConfig?.auth.aiProxyBackendKey const response = await fetch(url.toString(), { @@ -28,12 +26,13 @@ async function deleteToken(group: string, id: string): Promise { throw new Error(`HTTP error! status: ${response.status}`) } - const result: DeleteTokenResponse = await response.json() + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { - throw new Error(result.message || 'Failed to delete token') + throw new Error(result.message || 'API request failed') } } catch (error) { - console.error('Error deleting token:', error) + console.error('Error deleting group:', error) throw error } } @@ -41,32 +40,30 @@ async function deleteToken(group: string, id: string): Promise { export async function DELETE( request: NextRequest, { params }: { params: { id: string } } -): Promise { +): Promise> { try { - // 验证用户权限 - const userGroup = await parseJwtToken(request.headers) + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) - // 验证 ID 参数 if (!params.id) { return NextResponse.json( { code: 400, - message: 'Token ID is required', + message: 'Group id is required', error: 'Bad Request' }, { status: 400 } ) } - // 删除 Token - await deleteToken(userGroup, params.id) + await deleteGroup(params.id) return NextResponse.json({ code: 200, - message: 'Token deleted successfully' - }) + message: 'Group deleted successfully' + } satisfies ApiResp) } catch (error) { - console.error('Token deletion error:', error) + console.error('Groups search error:', error) return NextResponse.json( { code: 500, diff --git a/frontend/providers/aiproxy/app/api/admin/group/[id]/status/route.ts b/frontend/providers/aiproxy/app/api/admin/group/[id]/status/route.ts new file mode 100644 index 000000000000..1671e00afa82 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/group/[id]/status/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { GroupStatus } from '@/types/admin/group' + +export const dynamic = 'force-dynamic' + +async function updateGroup(status: GroupStatus, id: string): Promise { + try { + const url = new URL( + `/api/group/${id}/status`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify({ status: status }), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to update group status') + } + } catch (error) { + console.error('admin groups api: update group status error:## ', error) + throw error + } +} + +// update group status +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Group id is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + const { status }: { status: GroupStatus } = await request.json() + await updateGroup(status, params.id) + + return NextResponse.json({ + code: 200, + message: 'Group status updated successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin groups api: update group status error:## ', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/group/route.ts b/frontend/providers/aiproxy/app/api/admin/group/route.ts new file mode 100644 index 000000000000..6f3586197a59 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/group/route.ts @@ -0,0 +1,125 @@ +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { parseJwtToken } from '@/utils/backend/auth' +import { isAdmin } from '@/utils/backend/isAdmin' +import { NextRequest, NextResponse } from 'next/server' +import { GroupInfo } from '@/types/admin/group' + +export const dynamic = 'force-dynamic' + +export type ApiProxyBackendGroupSearchResponse = ApiProxyBackendResp<{ + groups: GroupInfo[] + total: number +}> + +export type GroupSearchResponse = ApiResp<{ + groups: GroupInfo[] + total: number +}> + +export interface GroupQueryParams { + keyword?: string + page: number + perPage: number +} + +function validateParams(params: GroupQueryParams): string | null { + if (params.page < 1) { + return 'Page number must be greater than 0' + } + if (params.perPage < 1 || params.perPage > 100) { + return 'Per page must be between 1 and 100' + } + return null +} + +async function fetchGroups( + params: GroupQueryParams +): Promise<{ groups: GroupInfo[]; total: number }> { + try { + const url = new URL( + `/api/groups/search`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + url.searchParams.append('p', params.page.toString()) + url.searchParams.append('per_page', params.perPage.toString()) + + if (params.keyword) { + url.searchParams.append('keyword', params.keyword) + } + + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendGroupSearchResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'API request failed') + } + + return { + groups: result.data?.groups || [], + total: result.data?.total || 0 + } + } catch (error) { + console.error('Error fetching groups:', error) + throw error + } +} + +export async function GET(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + const searchParams = request.nextUrl.searchParams + + const queryParams: GroupQueryParams = { + page: parseInt(searchParams.get('page') || '1', 10), + perPage: parseInt(searchParams.get('perPage') || '10', 10), + keyword: searchParams.get('keyword') || undefined + } + + const validationError = validateParams(queryParams) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + const { groups, total } = await fetchGroups(queryParams) + + return NextResponse.json({ + code: 200, + data: { + groups, + total + } + } satisfies GroupSearchResponse) + } catch (error) { + console.error('Groups search error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/get-logs/route.ts b/frontend/providers/aiproxy/app/api/admin/log/route.ts similarity index 73% rename from frontend/providers/aiproxy/app/api/get-logs/route.ts rename to frontend/providers/aiproxy/app/api/admin/log/route.ts index 5f9fd0a93be8..da52fe7275f6 100644 --- a/frontend/providers/aiproxy/app/api/get-logs/route.ts +++ b/frontend/providers/aiproxy/app/api/admin/log/route.ts @@ -1,28 +1,32 @@ -import { LogItem } from '@/types/log' -import { parseJwtToken } from '@/utils/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { GlobalLogItem } from '@/types/user/logs' +import { parseJwtToken } from '@/utils/backend/auth' +import { isAdmin } from '@/utils/backend/isAdmin' import { NextRequest, NextResponse } from 'next/server' export const dynamic = 'force-dynamic' -export interface SearchResponse { - data: { - logs: LogItem[] - total: number - } - message: string - success: boolean -} +export type ApiProxyBackendGlobalLogSearchResponse = ApiProxyBackendResp<{ + logs: GlobalLogItem[] + total: number +}> + +export type GlobalLogSearchResponse = ApiResp<{ + logs: GlobalLogItem[] + total: number +}> -export interface QueryParams { +export interface GlobalLogQueryParams { token_name?: string model_name?: string code?: string start_timestamp?: string end_timestamp?: string + group_id?: string page: number perPage: number } -function validateParams(params: QueryParams): string | null { +function validateParams(params: GlobalLogQueryParams): string | null { if (params.page < 1) { return 'Page number must be greater than 0' } @@ -38,12 +42,11 @@ function validateParams(params: QueryParams): string | null { } async function fetchLogs( - params: QueryParams, - group: string -): Promise<{ logs: LogItem[]; total: number }> { + params: GlobalLogQueryParams +): Promise<{ logs: GlobalLogItem[]; total: number }> { try { const url = new URL( - `/api/log/${group}/search`, + `/api/logs/search`, global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy ) @@ -59,6 +62,9 @@ async function fetchLogs( if (params.code) { url.searchParams.append('code', params.code) } + if (params.group_id) { + url.searchParams.append('group_id', params.group_id) + } if (params.start_timestamp) { url.searchParams.append('start_timestamp', params.start_timestamp) } @@ -81,14 +87,14 @@ async function fetchLogs( throw new Error(`HTTP error! status: ${response.status}`) } - const result: SearchResponse = await response.json() + const result: ApiProxyBackendGlobalLogSearchResponse = await response.json() if (!result.success) { throw new Error(result.message || 'API request failed') } return { - logs: result.data.logs, - total: result.data.total + logs: result.data?.logs || [], + total: result.data?.total || 0 } } catch (error) { console.error('Error fetching logs:', error) @@ -96,12 +102,13 @@ async function fetchLogs( } } -export async function GET(request: NextRequest): Promise { +export async function GET(request: NextRequest): Promise> { try { - const group = await parseJwtToken(request.headers) + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) const searchParams = request.nextUrl.searchParams - const queryParams: QueryParams = { + const queryParams: GlobalLogQueryParams = { page: parseInt(searchParams.get('page') || '1', 10), perPage: parseInt(searchParams.get('perPage') || '10', 10), token_name: searchParams.get('token_name') || undefined, @@ -123,7 +130,7 @@ export async function GET(request: NextRequest): Promise { ) } - const { logs, total } = await fetchLogs(queryParams, group) + const { logs, total } = await fetchLogs(queryParams) return NextResponse.json({ code: 200, @@ -131,7 +138,7 @@ export async function GET(request: NextRequest): Promise { logs, total } - }) + } satisfies GlobalLogSearchResponse) } catch (error) { console.error('Logs search error:', error) return NextResponse.json( diff --git a/frontend/providers/aiproxy/app/api/admin/option/batch/route.ts b/frontend/providers/aiproxy/app/api/admin/option/batch/route.ts new file mode 100644 index 000000000000..70210e5402af --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/option/batch/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { BatchOptionData } from '@/types/admin/option' + +export const dynamic = 'force-dynamic' + +async function batchOption(batchOptionData: BatchOptionData): Promise { + try { + const url = new URL( + `/api/option/batch`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify(batchOptionData), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to batch option') + } + + return result.message + } catch (error) { + console.error('admin batch options api: update option error:', error) + throw error + } +} + +function validateBatchOptionData(batchOptionData: BatchOptionData): boolean { + if (typeof batchOptionData.DefaultChannelModelMapping !== 'string') { + return false + } + if (typeof batchOptionData.DefaultChannelModels !== 'string') { + return false + } + return true +} + +export async function PUT(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + const body = await request.json() + if (!validateBatchOptionData(body)) { + return NextResponse.json( + { + code: 400, + message: 'Invalid request body', + error: 'Invalid request body' + } satisfies ApiResp, + { status: 400 } + ) + } + + await batchOption(body) + + return NextResponse.json({ + code: 200, + message: 'Option batch updated successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin batch options api: put option error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/option/route.ts b/frontend/providers/aiproxy/app/api/admin/option/route.ts new file mode 100644 index 000000000000..564ce5e72268 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/option/route.ts @@ -0,0 +1,149 @@ +import { NextRequest, NextResponse } from 'next/server' +import { OptionData } from '@/types/admin/option' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' + +export const dynamic = 'force-dynamic' + +type ApiProxyBackendOptionResponse = ApiProxyBackendResp + +export type GetOptionResponse = ApiResp + +async function fetchOptions(): Promise { + try { + const url = new URL( + `/api/option/`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendOptionResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'Failed to fetch options') + } + + return result.data + } catch (error) { + console.error('admin options api: fetch options error:', error) + throw error + } +} + +export async function GET(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + const optionData = await fetchOptions() + + return NextResponse.json({ + code: 200, + data: optionData + } satisfies GetOptionResponse) + } catch (error) { + console.error('admin options api: get options error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies GetOptionResponse, + { status: 500 } + ) + } +} + +async function updateOption(key: string, value: string): Promise { + try { + const url = new URL( + `/api/option`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify({ key, value }), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendOptionResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to update option') + } + + return result.message + } catch (error) { + console.error('admin options api: update option error:', error) + throw error + } +} + +export async function PUT(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + const body = await request.json() + if (!body.key || typeof body.key !== 'string') { + return NextResponse.json( + { + code: 400, + message: 'Invalid request body: key is required and must be a string', + error: 'Invalid request parameters' + } satisfies GetOptionResponse, + { status: 400 } + ) + } + + if (!body.value || typeof body.value !== 'string') { + return NextResponse.json( + { + code: 400, + message: 'Invalid request body: value is required and must be a string', + error: 'Invalid request parameters' + } satisfies GetOptionResponse, + { status: 400 } + ) + } + + await updateOption(body.key, body.value) + + return NextResponse.json({ + code: 200, + message: 'Option updated successfully' + } satisfies GetOptionResponse) + } catch (error) { + console.error('admin options api: put option error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies GetOptionResponse, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/admin/option/upload/route.ts b/frontend/providers/aiproxy/app/api/admin/option/upload/route.ts new file mode 100644 index 000000000000..799fdc1d8dc1 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/admin/option/upload/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { OptionData } from '@/types/admin/option' + +export const dynamic = 'force-dynamic' + +async function parseFormData(req: NextRequest): Promise { + try { + const formData = await req.formData() + const file = formData.get('file') + + if (!file || !(file instanceof File)) { + throw new Error('No file uploaded') + } + + const fileContent = await file.text() + const optionData = JSON.parse(fileContent) + + if (typeof optionData !== 'object' || optionData === null) { + throw new Error('Invalid file format: expected option data object') + } + + return optionData + } catch (error) { + throw error + } +} + +async function batchOption(batchOptionData: OptionData): Promise { + try { + const url = new URL( + `/api/option/batch`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify(batchOptionData), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to batch option') + } + + return result.message + } catch (error) { + console.error('admin batch options upload api: update option error:', error) + throw error + } +} + +export async function POST(request: NextRequest): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + const optionData = await parseFormData(request) + + await batchOption(optionData) + + return NextResponse.json({ + code: 200, + message: 'Option batch uploaded successfully' + } satisfies ApiResp) + } catch (error) { + console.error('admin batch options upload api: put option error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/create-key/route.ts b/frontend/providers/aiproxy/app/api/create-key/route.ts deleted file mode 100644 index 89161f4457e2..000000000000 --- a/frontend/providers/aiproxy/app/api/create-key/route.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { parseJwtToken } from '@/utils/auth' - -export const dynamic = 'force-dynamic' - -interface CreateTokenRequest { - name: string -} - -interface TokenInfo { - id: number - group: string - key: string - status: number - name: string - quota: number - used_amount: number - request_count: number - models: string[] | null - subnet: string - created_at: number - accessed_at: number - expired_at: number -} - -interface CreateTokenResponse { - data: TokenInfo - message: string - success: boolean -} - -function validateCreateParams(body: CreateTokenRequest): string | null { - if (!body.name) { - return 'Name parameter is required' - } - return null -} - -async function createToken(name: string, group: string): Promise { - try { - const url = new URL( - `/api/token/${group}?auto_create_group=true`, - global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy - ) - const token = global.AppConfig?.auth.aiProxyBackendKey - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}` - }, - cache: 'no-store', - body: JSON.stringify({ - name - }) - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const result: CreateTokenResponse = await response.json() - if (!result.success) { - throw new Error(result.message || 'Failed to create token') - } - - return result.data - } catch (error) { - console.error('Error creating token:', error) - throw error - } -} - -export async function POST(request: NextRequest): Promise { - try { - const group = await parseJwtToken(request.headers) - const body: CreateTokenRequest = await request.json() - - const validationError = validateCreateParams(body) - if (validationError) { - return NextResponse.json( - { - code: 400, - message: validationError, - error: validationError - }, - { status: 400 } - ) - } - - // 创建Token - const newToken = await createToken(body.name, group) - - return NextResponse.json({ - code: 200, - data: newToken, - message: 'Token created successfully' - }) - } catch (error) { - console.error('Token creation error:', error) - return NextResponse.json( - { - code: 500, - message: error instanceof Error ? error.message : 'Internal server error', - error: error instanceof Error ? error.message : 'Internal server error' - }, - { status: 500 } - ) - } -} diff --git a/frontend/providers/aiproxy/app/api/get-keys/route.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts deleted file mode 100644 index 13d3ff9c1363..000000000000 --- a/frontend/providers/aiproxy/app/api/get-keys/route.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { TokenInfo } from '@/types/getKeys' - -import { parseJwtToken } from '@/utils/auth' - -export const dynamic = 'force-dynamic' -export interface KeysSearchResponse { - data: { - tokens: TokenInfo[] - total: number - } - message: string - success: boolean -} - -export interface QueryParams { - page: number - perPage: number -} - -function validateParams(page: number, perPage: number): string | null { - if (page < 1) { - return 'Page number must be greater than 0' - } - - if (perPage < 1 || perPage > 100) { - return 'Per page must be between 1 and 100' - } - - return null -} - -async function fetchTokens( - page: number, - perPage: number, - group: string -): Promise<{ tokens: TokenInfo[]; total: number }> { - try { - const url = new URL( - `/api/token/${group}/search`, - global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy - ) - url.searchParams.append('p', page.toString()) - url.searchParams.append('per_page', perPage.toString()) - - const token = global.AppConfig?.auth.aiProxyBackendKey - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}` - }, - cache: 'no-store' - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const result: KeysSearchResponse = await response.json() - - if (!result.success) { - throw new Error(result.message || 'API request failed') - } - - return { - tokens: result.data.tokens.sort((a, b) => a.name.localeCompare(b.name)), - total: result.data.total - } - } catch (error) { - console.error('Error fetching tokens:', error) - return { - tokens: [], - total: 0 - } - } -} - -export async function GET(request: NextRequest): Promise { - try { - const group = await parseJwtToken(request.headers) - - const searchParams = request.nextUrl.searchParams - const page = parseInt(searchParams.get('page') || '1', 10) - const perPage = parseInt(searchParams.get('perPage') || '10', 10) - - const validationError = validateParams(page, perPage) - if (validationError) { - return NextResponse.json( - { - code: 400, - message: validationError, - error: validationError - }, - { status: 400 } - ) - } - - const { tokens, total } = await fetchTokens(page, perPage, group) - - return NextResponse.json({ - code: 200, - data: { - tokens, - total - } - }) - } catch (error) { - console.error('Token search error:', error) - - return NextResponse.json( - { - code: 500, - message: error instanceof Error ? error.message : 'Internal server error', - error: error instanceof Error ? error.message : 'Internal server error' - }, - { status: 500 } - ) - } -} diff --git a/frontend/providers/aiproxy/app/api/get-mode-price/route.ts b/frontend/providers/aiproxy/app/api/get-mode-price/route.ts deleted file mode 100644 index f9b6e0680e55..000000000000 --- a/frontend/providers/aiproxy/app/api/get-mode-price/route.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { parseJwtToken } from '@/utils/auth' -import { ModelPrice } from '@/types/backend' - -export const dynamic = 'force-dynamic' - -interface PriceResponse { - data: Record< - string, - { - prompt: number - completion: number - } - > - message: string - success: boolean -} - -function transformToList( - data: Record -): ModelPrice[] { - return Object.entries(data).map(([name, prices]) => ({ - name, - prompt: prices.prompt, - completion: prices.completion - })) -} - -async function fetchModelPrices(): Promise { - try { - const url = new URL( - `/api/models/enabled/price`, - global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy - ) - const token = global.AppConfig?.auth.aiProxyBackendKey - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}` - }, - cache: 'no-store' - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const result: PriceResponse = await response.json() - - if (!result.success) { - throw new Error(result.message || 'get model prices API request failed') - } - - return transformToList(result.data) - } catch (error) { - console.error('Error fetching model prices:', error) - return Promise.reject(error) - } -} - -export async function GET(request: NextRequest): Promise { - try { - await parseJwtToken(request.headers) - const modelPrices = await fetchModelPrices() - - return NextResponse.json({ - code: 200, - data: modelPrices - }) - } catch (error) { - console.error('get model prices error:', error) - return NextResponse.json( - { - code: 500, - message: error instanceof Error ? error.message : 'Internal server error', - error: error instanceof Error ? error.message : 'Internal server error' - }, - { status: 500 } - ) - } -} diff --git a/frontend/providers/aiproxy/app/api/get-models/route.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts deleted file mode 100644 index 7f2d205fb489..000000000000 --- a/frontend/providers/aiproxy/app/api/get-models/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { parseJwtToken } from '@/utils/auth' - -export const dynamic = 'force-dynamic' - -interface SearchResponse { - data: string[] - message: string - success: boolean -} - -async function fetchModels(): Promise { - try { - const url = new URL( - `/api/models/enabled`, - global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy - ) - const token = global.AppConfig?.auth.aiProxyBackendKey - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}` - }, - cache: 'no-store' - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const result: SearchResponse = await response.json() - - if (!result.success) { - throw new Error(result.message || 'get models API request failed') - } - - return result.data.sort((a, b) => a.localeCompare(b)) - } catch (error) { - console.error('Error fetching models:', error) - return Promise.reject(error) - } -} - -export async function GET(request: NextRequest): Promise { - try { - await parseJwtToken(request.headers) - - const models = await fetchModels() - - return NextResponse.json({ - code: 200, - data: models - }) - } catch (error) { - console.error('get models error:', error) - - return NextResponse.json( - { - code: 500, - message: error instanceof Error ? error.message : 'Internal server error', - error: error instanceof Error ? error.message : 'Internal server error' - }, - { status: 500 } - ) - } -} diff --git a/frontend/providers/aiproxy/app/api/init-app-config/route.ts b/frontend/providers/aiproxy/app/api/init-app-config/route.ts index 359ad4d4ec33..705e9c9f9371 100644 --- a/frontend/providers/aiproxy/app/api/init-app-config/route.ts +++ b/frontend/providers/aiproxy/app/api/init-app-config/route.ts @@ -1,9 +1,13 @@ import { NextResponse } from 'next/server' -import type { AppConfigType } from '@/types/appConfig' +import type { AppConfigType } from '@/types/app-config' export const dynamic = 'force-dynamic' +function getAdminNamespaces(): string[] { + return process.env.ADMIN_NAMESPACES?.split(',') || [] +} + function getAppConfig(appConfig: AppConfigType): AppConfigType { if (process.env.APP_TOKEN_JWT_KEY) { appConfig.auth.appTokenJwtKey = process.env.APP_TOKEN_JWT_KEY @@ -17,30 +21,58 @@ function getAppConfig(appConfig: AppConfigType): AppConfigType { if (process.env.AI_PROXY_BACKEND_INTERNAL) { appConfig.backend.aiproxyInternal = process.env.AI_PROXY_BACKEND_INTERNAL } + if (process.env.ADMIN_NAMESPACES) { + appConfig.adminNameSpace = getAdminNamespaces() + } if (process.env.CURRENCY_SYMBOL) { appConfig.currencySymbol = process.env.CURRENCY_SYMBOL as 'shellCoin' | 'cny' | 'usd' } + if (process.env.ACCOUNT_SERVER) { + appConfig.backend.accountServer = process.env.ACCOUNT_SERVER + } + if (process.env.ACCOUNT_SERVER_TOKEN_JWT_KEY) { + appConfig.auth.accountServerTokenJwtKey = process.env.ACCOUNT_SERVER_TOKEN_JWT_KEY + } + if (process.env.DOC_URL) { + appConfig.common.docUrl = process.env.DOC_URL + } + if (process.env.IS_INVITATION_ACTIVE) { + appConfig.common.isInvitationActive = process.env.IS_INVITATION_ACTIVE === 'true' + } + if (process.env.INVITATION_URL) { + appConfig.common.invitationUrl = process.env.INVITATION_URL + } + return appConfig } function initAppConfig(): AppConfigType { // default config const DefaultAppConfig: AppConfigType = { + common: { + docUrl: '', + isInvitationActive: false, + invitationUrl: '' + }, auth: { appTokenJwtKey: '', - aiProxyBackendKey: '' + aiProxyBackendKey: '', + accountServerTokenJwtKey: '' }, backend: { aiproxy: '', - aiproxyInternal: '' + aiproxyInternal: '', + accountServer: '' }, + adminNameSpace: [], currencySymbol: 'shellCoin' } + if (!global.AppConfig) { try { global.AppConfig = getAppConfig(DefaultAppConfig) } catch (error) { - console.error('Config initialization error:', error) + console.error('init-app-config: Config initialization error:', error) global.AppConfig = DefaultAppConfig } } @@ -57,7 +89,10 @@ export async function GET(): Promise { message: 'Success', data: { aiproxyBackend: config.backend.aiproxy, - currencySymbol: config.currencySymbol + currencySymbol: config.currencySymbol, + docUrl: config.common.docUrl, + isInvitationActive: config.common.isInvitationActive, + invitationUrl: config.common.invitationUrl } }) } catch (error) { diff --git a/frontend/providers/aiproxy/app/api/models/builtin/channel/route.ts b/frontend/providers/aiproxy/app/api/models/builtin/channel/route.ts new file mode 100644 index 000000000000..b425fe6b76b0 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/models/builtin/channel/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { ChannelWithMode } from '@/types/models/model' + +export const dynamic = 'force-dynamic' + +type ApiProxyBackendAllChannelEnabledModeResponse = ApiProxyBackendResp +export type GetAllChannelEnabledModelsResponse = ApiResp + +async function fetchAllChannelEnabledModels(): Promise { + try { + const url = new URL( + '/api/models/builtin/channel', + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${global.AppConfig?.auth.aiProxyBackendKey}` + }, + cache: 'no-store' + }) + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + const result: ApiProxyBackendAllChannelEnabledModeResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'builtin channel api: ai proxy backend error') + } + + return result.data || {} + } catch (error) { + console.error('builtin channel api: fetch enabled models error:', error) + throw error + } +} + +export async function GET( + request: NextRequest +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + return NextResponse.json({ + code: 200, + data: await fetchAllChannelEnabledModels() + } satisfies GetAllChannelEnabledModelsResponse) + } catch (error) { + console.error('builtin channel api: get enabled models error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/models/default/route.ts b/frontend/providers/aiproxy/app/api/models/default/route.ts new file mode 100644 index 000000000000..02eccc49e7fe --- /dev/null +++ b/frontend/providers/aiproxy/app/api/models/default/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { isAdmin } from '@/utils/backend/isAdmin' +import { ChannelWithDefaultModelAndDefaultModeMapping } from '@/types/models/model' + +export const dynamic = 'force-dynamic' + +type ApiProxyBackendDefaultModelAndModeMappingResponse = + ApiProxyBackendResp +export type GetDefaultModelAndModeMappingResponse = + ApiResp + +async function fetchDefaultModeAndModeMapping(): Promise< + ChannelWithDefaultModelAndDefaultModeMapping | undefined +> { + try { + const url = new URL( + '/api/models/default', + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${global.AppConfig?.auth.aiProxyBackendKey}` + }, + cache: 'no-store' + }) + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + const result: ApiProxyBackendDefaultModelAndModeMappingResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'default enabled models api: ai proxy backend error') + } + + return result?.data + } catch (error) { + console.error('default enabled models api: fetch enabled models error:', error) + throw error + } +} + +export async function GET( + request: NextRequest +): Promise> { + try { + const namespace = await parseJwtToken(request.headers) + await isAdmin(namespace) + + return NextResponse.json({ + code: 200, + data: await fetchDefaultModeAndModeMapping() + }) + } catch (error) { + console.error('default enabled models api: get enabled models error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/models/enabled/route.ts b/frontend/providers/aiproxy/app/api/models/enabled/route.ts new file mode 100644 index 000000000000..e2a78a758642 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/models/enabled/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { ModelConfig } from '@/types/models/model' + +type ApiProxyBackendEnabledModelsResponse = ApiProxyBackendResp +export type GetEnabledModelsResponse = ApiResp + +export const dynamic = 'force-dynamic' + +async function fetchEnabledModels(namespace: string): Promise { + try { + const url = new URL( + `/api/dashboard/${namespace}/models`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${global.AppConfig?.auth.aiProxyBackendKey}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error, status code: ${response.status}`) + } + + const result: ApiProxyBackendEnabledModelsResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'enabled models api: ai proxy backend error') + } + + return result.data || [] + } catch (error) { + console.error('enabled models api: fetch enabled models error:', error) + throw error + } +} + +export async function GET(request: NextRequest): Promise> { + try { + const group = await parseJwtToken(request.headers) + + return NextResponse.json({ + code: 200, + data: await fetchEnabledModels(group) + } satisfies GetEnabledModelsResponse) + } catch (error) { + console.error('enabled models api: get enabled models error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'server error', + error: error instanceof Error ? error.message : 'server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts deleted file mode 100644 index c0665abd3ff2..000000000000 --- a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { parseJwtToken } from '@/utils/auth' - -export const dynamic = 'force-dynamic' -interface UpdateTokenResponse { - message: string - success: boolean -} - -interface UpdateTokenBody { - status: number -} - -async function updateToken(group: string, id: string, status: number): Promise { - try { - if (status !== 1 && status !== 2) { - throw new Error('Invalid status') - } - const url = new URL( - `/api/token/${group}/${id}/status`, - global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy - ) - const token = global.AppConfig?.auth.aiProxyBackendKey - - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}` - }, - body: JSON.stringify({ status }), - cache: 'no-store' - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const result: UpdateTokenResponse = await response.json() - if (!result.success) { - throw new Error(result.message || 'Failed to update token') - } - } catch (error) { - console.error('Error updating token:', error) - throw error - } -} - -export async function POST( - request: NextRequest, - { params }: { params: { id: string } } -): Promise { - try { - // 验证用户权限 - const userGroup = await parseJwtToken(request.headers) - - // 验证 ID 参数 - if (!params.id) { - return NextResponse.json( - { - code: 400, - message: 'Token ID is required', - error: 'Bad Request' - }, - { status: 400 } - ) - } - - // 获取请求体 - const body: UpdateTokenBody = await request.json() - - // 验证状态参数 - if (typeof body.status !== 'number') { - return NextResponse.json( - { - code: 400, - message: 'Status must be a number', - error: 'Bad Request' - }, - { status: 400 } - ) - } - - // 更新 Token - await updateToken(userGroup, params.id, body.status) - - return NextResponse.json({ - code: 200, - message: 'Token updated successfully' - }) - } catch (error) { - console.error('Token update error:', error) - return NextResponse.json( - { - code: 500, - message: error instanceof Error ? error.message : 'Internal server error', - error: error instanceof Error ? error.message : 'Internal server error' - }, - { status: 500 } - ) - } -} diff --git a/frontend/providers/aiproxy/app/api/user/dashboard/route.ts b/frontend/providers/aiproxy/app/api/user/dashboard/route.ts new file mode 100644 index 000000000000..2fbbd104e036 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/dashboard/route.ts @@ -0,0 +1,129 @@ +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/backend/auth' +import { DashboardData } from '@/types/user/dashboard' +import { DashboardResponse } from '@/types/user/dashboard' + +// 定义API响应数据结构 +export type ApiProxyBackendDashboardResponse = ApiProxyBackendResp + +// 定义查询参数接口 +export interface DashboardQueryParams { + type: 'day' | 'week' | 'two_week' | 'month' + model?: string + token_name?: string +} + +// 验证查询参数 +function validateParams(params: DashboardQueryParams): string | null { + if (!params.type) { + return 'Type parameter is required' + } + if ( + params.type !== 'day' && + params.type !== 'week' && + params.type !== 'two_week' && + params.type !== 'month' + ) { + return 'Invalid type parameter. Must be one of: day, week, two_week, month' + } + return null +} + +// 获取仪表盘数据 +async function fetchDashboardData( + params: DashboardQueryParams, + group: string +): Promise { + try { + const url = new URL( + `/api/dashboard/${group}`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + url.searchParams.append('type', params.type) + if (params.model) { + url.searchParams.append('model', params.model) + } + + if (params.token_name) { + url.searchParams.append('token_name', params.token_name) + } + + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendDashboardResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'API request failed') + } + + return { + chart_data: result.data?.chart_data || [], + token_names: result.data?.token_names || [], + models: result.data?.models || [], + total_count: result.data?.total_count || 0, + exception_count: result.data?.exception_count || 0, + used_amount: result.data?.used_amount || 0, + rpm: result.data?.rpm || 0, + tpm: result.data?.tpm || 0 + } + } catch (error) { + console.error('Error fetching dashboard data:', error) + throw error + } +} + +export async function GET(request: NextRequest): Promise> { + try { + const group = await parseJwtToken(request.headers) + const searchParams = request.nextUrl.searchParams + + const queryParams: DashboardQueryParams = { + type: (searchParams.get('type') as 'day' | 'week' | 'two_week' | 'month') || 'week', + model: searchParams.get('model') || undefined, + token_name: searchParams.get('token_name') || undefined + } + + const validationError = validateParams(queryParams) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + const dashboardData = await fetchDashboardData(queryParams, group) + + return NextResponse.json({ + code: 200, + data: dashboardData + } satisfies DashboardResponse) + } catch (error) { + console.error('Dashboard fetch error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/user/log/detail/[log_id]/route.ts b/frontend/providers/aiproxy/app/api/user/log/detail/[log_id]/route.ts new file mode 100644 index 000000000000..a0bb12a9f688 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/log/detail/[log_id]/route.ts @@ -0,0 +1,85 @@ +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { RequestDetail } from '@/types/user/logs' +import { parseJwtToken } from '@/utils/backend/auth' +import { NextRequest, NextResponse } from 'next/server' + +export const dynamic = 'force-dynamic' +export type ApiProxyBackendUserLogDetailResponse = ApiProxyBackendResp + +export type UserLogDetailResponse = ApiResp + +export interface UserLogDetailParams { + log_id: string +} + +async function fetchLogs(log_id: string, group: string): Promise { + try { + const url = new URL( + `/api/log/${group}/detail/${log_id}`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendUserLogDetailResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'Get logs detail API request failed') + } + + return result.data || null + } catch (error) { + console.error('Get logs detail error:', error) + throw error + } +} + +export async function GET( + request: NextRequest, + { params }: { params: { log_id: string } } +): Promise> { + try { + const group = await parseJwtToken(request.headers) + + if (!params.log_id) { + return NextResponse.json( + { + code: 400, + message: 'Log_id is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + const detail = await fetchLogs(params.log_id, group) + + return NextResponse.json({ + code: 200, + data: detail || undefined + } satisfies UserLogDetailResponse) + } catch (error) { + console.error('Get logs detail error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/user/log/route.ts b/frontend/providers/aiproxy/app/api/user/log/route.ts new file mode 100644 index 000000000000..ce0c1909bec9 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/log/route.ts @@ -0,0 +1,163 @@ +import { ApiProxyBackendResp, ApiResp } from '@/types/api' +import { LogItem } from '@/types/user/logs' +import { parseJwtToken } from '@/utils/backend/auth' +import { NextRequest, NextResponse } from 'next/server' + +export const dynamic = 'force-dynamic' +export type ApiProxyBackendUserLogSearchResponse = ApiProxyBackendResp<{ + logs: LogItem[] + total: number + models: string[] + token_names: string[] +}> + +export type UserLogSearchResponse = ApiResp<{ + logs: LogItem[] + total: number + models: string[] + token_names: string[] +}> + +export interface UserLogQueryParams { + token_name?: string + model_name?: string + keyword?: string + start_timestamp?: string + end_timestamp?: string + code_type?: 'all' | 'success' | 'error' | undefined + page: number + perPage: number +} + +function validateParams(params: UserLogQueryParams): string | null { + if (params.page < 1) { + return 'Page number must be greater than 0' + } + if (params.perPage < 1 || params.perPage > 100) { + return 'Per page must be between 1 and 100' + } + if (params.start_timestamp && params.end_timestamp) { + if (parseInt(params.start_timestamp) > parseInt(params.end_timestamp)) { + return 'Start timestamp cannot be greater than end timestamp' + } + } + return null +} + +async function fetchLogs( + params: UserLogQueryParams, + group: string +): Promise<{ logs: LogItem[]; total: number; models: string[]; token_names: string[] }> { + try { + const url = new URL( + `/api/log/${group}/search`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + url.searchParams.append('p', params.page.toString()) + url.searchParams.append('per_page', params.perPage.toString()) + + if (params.token_name) { + url.searchParams.append('token_name', params.token_name) + } + if (params.model_name) { + url.searchParams.append('model_name', params.model_name) + } + + if (params.keyword) { + url.searchParams.append('keyword', params.keyword) + } + + if (params.code_type) { + url.searchParams.append('code_type', params.code_type) + } + if (params.start_timestamp) { + url.searchParams.append('start_timestamp', params.start_timestamp) + } + if (params.end_timestamp) { + url.searchParams.append('end_timestamp', params.end_timestamp) + } + + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendUserLogSearchResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'API request failed') + } + + return { + logs: result.data?.logs || [], + total: result.data?.total || 0, + models: result.data?.models || [], + token_names: result.data?.token_names || [] + } + } catch (error) { + console.error('Error fetching logs:', error) + throw error + } +} + +export async function GET(request: NextRequest): Promise> { + try { + const group = await parseJwtToken(request.headers) + const searchParams = request.nextUrl.searchParams + + const queryParams: UserLogQueryParams = { + page: parseInt(searchParams.get('page') || '1', 10), + perPage: parseInt(searchParams.get('perPage') || '10', 10), + token_name: searchParams.get('token_name') || undefined, + model_name: searchParams.get('model_name') || undefined, + code_type: (searchParams.get('code_type') as 'all' | 'success' | 'error') || undefined, + start_timestamp: searchParams.get('start_timestamp') || undefined, + end_timestamp: searchParams.get('end_timestamp') || undefined, + keyword: searchParams.get('keyword') || undefined + } + + const validationError = validateParams(queryParams) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + const { logs, total, models, token_names } = await fetchLogs(queryParams, group) + + return NextResponse.json({ + code: 200, + data: { + logs, + total, + models, + token_names + } + } satisfies UserLogSearchResponse) + } catch (error) { + console.error('Logs search error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/user/token/[id]/route.ts b/frontend/providers/aiproxy/app/api/user/token/[id]/route.ts new file mode 100644 index 000000000000..b02f58c04738 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/token/[id]/route.ts @@ -0,0 +1,176 @@ +import { NextRequest, NextResponse } from 'next/server' +import { checkSealosUserIsRealName, parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' + +export const dynamic = 'force-dynamic' + +async function deleteToken(group: string, id: string): Promise { + try { + const url = new URL( + `/api/token/${group}/${id}`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to delete token') + } + } catch (error) { + console.error('Error deleting token:', error) + throw error + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +): Promise> { + try { + const userGroup = await parseJwtToken(request.headers) + + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Token ID is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + await deleteToken(userGroup, params.id) + + return NextResponse.json({ + code: 200, + message: 'Token deleted successfully' + } satisfies ApiResp) + } catch (error) { + console.error('Token deletion error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} + +// update token status +interface UpdateTokenRequestBody { + status: number +} + +async function updateToken(group: string, id: string, status: number): Promise { + try { + if (status !== 1 && status !== 2) { + throw new Error('Invalid status') + } + const url = new URL( + `/api/token/${group}/${id}/status`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify({ status }), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to update token') + } + } catch (error) { + console.error('Error updating token:', error) + throw error + } +} + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +): Promise> { + try { + const userGroup = await parseJwtToken(request.headers) + + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Token ID is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + const isRealName = await checkSealosUserIsRealName(request.headers) + + if (!isRealName) { + return NextResponse.json( + { + code: 400, + message: 'key.userNotRealName', + error: 'key.userNotRealName' + }, + { status: 400 } + ) + } + + const updateTokenBody: UpdateTokenRequestBody = await request.json() + + if (typeof updateTokenBody.status !== 'number') { + return NextResponse.json( + { + code: 400, + message: 'Status must be a number', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + await updateToken(userGroup, params.id, updateTokenBody.status) + + return NextResponse.json({ + code: 200, + message: 'Token updated successfully' + } satisfies ApiResp) + } catch (error) { + console.error('Token update error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/user/token/route.ts b/frontend/providers/aiproxy/app/api/user/token/route.ts new file mode 100644 index 000000000000..10791109f67f --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/token/route.ts @@ -0,0 +1,224 @@ +import { NextRequest, NextResponse } from 'next/server' +import { TokenInfo } from '@/types/user/token' + +import { checkSealosUserIsRealName, parseJwtToken } from '@/utils/backend/auth' +import { ApiProxyBackendResp, ApiResp } from '@/types/api' + +export const dynamic = 'force-dynamic' + +type ApiProxyBackendTokenSearchResponse = ApiProxyBackendResp<{ + tokens: TokenInfo[] + total: number +}> + +export type GetTokensResponse = ApiResp<{ + tokens: TokenInfo[] + total: number +}> + +export interface GetTokensQueryParams { + page: number + perPage: number +} + +function validateParams(queryParams: GetTokensQueryParams): string | null { + if (queryParams.page < 1) { + return 'Page number must be greater than 0' + } + + if (queryParams.perPage < 1 || queryParams.perPage > 100) { + return 'Per page must be between 1 and 100' + } + + return null +} + +async function fetchTokens( + queryParams: GetTokensQueryParams, + group: string +): Promise<{ tokens: TokenInfo[]; total: number }> { + try { + const url = new URL( + `/api/token/${group}/search`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + url.searchParams.append('p', queryParams.page.toString()) + url.searchParams.append('per_page', queryParams.perPage.toString()) + + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendTokenSearchResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'API request failed') + } + + return { + tokens: result?.data?.tokens || [], + total: result?.data?.total || 0 + } + } catch (error) { + console.error('Error fetching tokens:', error) + return { + tokens: [], + total: 0 + } + } +} + +export async function GET(request: NextRequest): Promise> { + try { + const group = await parseJwtToken(request.headers) + + const searchParams = request.nextUrl.searchParams + const queryParams: GetTokensQueryParams = { + page: parseInt(searchParams.get('page') || '1', 10), + perPage: parseInt(searchParams.get('perPage') || '10', 10) + } + + const validationError = validateParams(queryParams) + + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + const { tokens, total } = await fetchTokens(queryParams, group) + + return NextResponse.json({ + code: 200, + data: { + tokens, + total + } + } satisfies GetTokensResponse) + } catch (error) { + console.error('Token search error:', error) + + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} + +// create token + +interface CreateTokenRequest { + name: string +} + +function validateCreateParams(body: CreateTokenRequest): string | null { + if (!body.name) { + return 'Name parameter is required' + } + return null +} + +async function createToken(name: string, group: string): Promise { + try { + const url = new URL( + `/api/token/${group}?auto_create_group=true`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store', + body: JSON.stringify({ + name + }) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiProxyBackendResp = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to create token') + } + + return result?.data + } catch (error) { + console.error('Error creating token:', error) + throw error + } +} + +export async function POST(request: NextRequest): Promise>> { + try { + const group = await parseJwtToken(request.headers) + const body: CreateTokenRequest = await request.json() + + const validationError = validateCreateParams(body) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + } satisfies ApiResp, + { status: 400 } + ) + } + + const isRealName = await checkSealosUserIsRealName(request.headers) + + if (!isRealName) { + return NextResponse.json( + { + code: 400, + message: 'key.userNotRealName', + error: 'key.userNotRealName' + }, + { status: 400 } + ) + } + // 创建Token + const newToken = await createToken(body.name, group) + + return NextResponse.json({ + code: 200, + data: newToken, + message: 'Token created successfully' + } satisfies ApiResp) + } catch (error) { + console.error('Token creation error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + } satisfies ApiResp, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index e5d2271905a4..3914a94820cb 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -2,12 +2,23 @@ "title": "AI proxy", "description": "AI proxy", "Sidebar": { - "Home": "API Keys", + "Home": "Dashboard", "Logs": "Logs", - "Price": "Pricing" + "Price": "Models", + "Dashboard": "Channels", + "GlobalLogs": "Logs", + "GlobalConfigs": "Config", + "NsManager": "NS Manage", + "Keys": "API Keys" }, "keyList": { - "title": "API Keys" + "title": "API Keys", + "invitationText1": "Limited time benefits!!", + "invite": "Event details", + "invitationText2": "Invite friends to register Sealos,", + "invitationText3": "Rebate immediately: 10 yuan balance!", + "invitationText4": "Invitation link:", + "invitationText5": "Click to copy" }, "key": { "key": "API Key", @@ -15,8 +26,8 @@ "createdAt": "Creation time", "lastUsedAt": "Last use time", "status": "State", - "namePlaceholder": "Please enter name", - "nameRequired": "Please enter key name", + "namePlaceholder": "please enter name", + "nameRequired": "please enter key name", "nameMaxLength": "Key length is illegal", "nameOnlyLettersAndNumbers": "key name contains special characters", "createSuccess": "Created successfully", @@ -29,29 +40,53 @@ "unused": "Not use", "inputPrice": "Input price", "outputPrice": "Output price", - "createName": "Name" + "createName": "Name", + "modelType": "Type", + "requestCount": "Request Count", + "usedAmount": "Amount Charged", + "userNotRealName": "AI services require real-name authentication." }, "logs": { "call_log": "Logs", - "name": "Name", + "name": "Key Name", "status": "State", "time": "Time", "modal": "Model", - "prompt_tokens": "Input", - "completion_tokens": "Output", + "prompt_tokens": "Input Tokens", + "completion_tokens": "Output Tokens", "price": "Price", - "select_modal": "Please select model", - "select_token_name": "Please select name", + "select_modal": "please select model", + "select_token_name": "please select name", "model": "Model", - "total_price": "Total amount", + "total_price": "Total Charged", "total_price_tip": "(Number of input tokens × Input price) + (Number of output tokens × Output price)", "success": "Success", - "failed": "Failed" + "failed": "Failed", + "logDetail": "Log details", + "actions": "Operation", + "detail": "Details", + "requestId": "RequestID", + "mode": "Type", + "requestTime": "Request At", + "totalTime": "Duration (s)", + "tokenName": "Key name", + "tokenId": "Key ID", + "info": "Message", + "requestBody": "Request Body", + "responseBody": "Response Body", + "inputTokens": "Input Tokens", + "outputTokens": "Output Tokens", + "searchByContent": "Enter search keyword", + "statusOptions": { + "all": "All", + "success": "Success", + "error": "Error" + } }, "Page": "Page", "Total": "Total", "modelList": { - "title": "Supported models" + "title": "Supported Models" }, "copy": "Copy", "createKey": "New", @@ -73,19 +108,191 @@ "copyFailed": "Copy failed", "noData": "You don’t have an API Key yet", "price": { - "title": "Pricing", - "per1kTokens": "1k tokens" - }, - "ernie": "Baidu-Ernie", - "qwen": "Alibaba-Qwen", - "chatglm": "BigModel-Chatglm", - "deepseek": "Deepseek", - "moonshot": "Moonshot", - "sparkdesk": "Sparkdesk", - "abab": "Minimax", - "doubao": "ByteDance-Doubao", - "glm": "Glm", - "o": "OpenAI", - "gpt": "OpenAI", - "createKey2": "Create Key" + "all": "All", + "title": "Models", + "per1kTokens": "1k tokens", + "modelOwner": "Series/Manufacturer", + "modelType": "Type", + "modelName": "Model Name", + "modelRpm": "RPM", + "modelRpmTooltip": "Each model has its own RPM", + "sortUpTooltip": "Click to view in ascending order", + "sortDownTooltip": "Click to view in Descending order", + "modelVision": "Vision", + "modelVisionLabel": "This model supports vision capabilities", + "modelToolChoice": "Tool Chocice", + "response": "Output" + }, + "createKey2": "Create Key", + "dashboard": { + "title": "Channels", + "create": "New", + "import": "Import", + "export": "Export", + "exportAll": "Export All", + "importSuccess": "Import successful", + "importError": "Import failed" + }, + "channelStatus": { + "autoDisabled": "System disabled" + }, + "channels": { + "name": "Name", + "type": "Manufacturer", + "test": "Test", + "disable": "Disable", + "enable": "Enable", + "edit": "Edit", + "export": "Export", + "create": "New", + "name_required": "The name is illegal", + "key_required": "Key is illegal", + "modelDefault": "Channel default model", + "createFailed": "Failed to create channel", + "createSuccess": "Channel created successfully", + "updateSuccess": "Update channel successful", + "updateFailed": "Update channel failed", + "id": "ID", + "requestCount": "Request Times", + "status": "Status", + "action": "Operation", + "delete": "Delete" + }, + "channelsForm": { + "name": "Custom Name", + "type": "Manufacturer", + "models": "Model", + "model_mapping": "Input Mode & Output Mode", + "add": "Add", + "key": "Key", + "base_url": "Proxy Address" + }, + "channelsFormPlaceholder": { + "name": "custom name", + "type": "open ai", + "model": "enter selection", + "modelInput": "enter custom model name", + "modelMappingInput": "input model", + "modelMappingOutput": "output model", + "key": "please enter the channel key", + "base_url": "please enter agent" + }, + "common": { + "add": "Add" + }, + "global_configs": { + "title": "Global configuration", + "export": "Export", + "import": "Import", + "qpm_limit": "Global QPM limits", + "pause_service": "Service Pause", + "retry_count": "Number of error retries", + "max_token": "Maximum number of tokens in the workspace" + }, + "globalConfigs": { + "defaultModel": "Default Model", + "addDefaultModel": "New", + "saveDefaultModel": "Save", + "saveDefaultModelFailed": "Failed To Save", + "saveDefaultModelSuccess": "Saved successfully", + "saveCommonConfigSuccess": "Update successful", + "saveCommonConfigFailed": "Update failed", + "common_config": "Common Configuration", + "model_config": "Model Settings" + }, + "modeType": { + "0": "Unknown", + "1": "Chat", + "2": "Text", + "3": "Embed", + "4": "Moderate", + "5": "Image", + "6": "Edit", + "7": "TTS", + "8": "STT", + "9": "Audio", + "10": "Rerank" + }, + "modeOwner": { + "openai": "OpenAI", + "alibaba": "Alibaba", + "tencent": "Tencent", + "xunfei": "iFlytek", + "deepseek": "DeepSeek", + "moonshot": "Moonshot AI", + "minimax": "MiniMax", + "baidu": "Baidu", + "google": "Google", + "baai": "BAAI", + "funaudiollm": "FunAudio LLM", + "doubao": "Doubao", + "fishaudio": "Fish Audio", + "chatglm": "ChatGLM", + "stabilityai": "Stability AI", + "netease": "NetEase", + "ai360": "360 AI", + "anthropic": "Anthropic", + "meta": "Meta", + "baichuan": "Baichuan", + "mistral": "Mistral AI", + "openchat": "OpenChat", + "microsoft": "Microsoft", + "defog": "Defog", + "nexusflow": "NexusFlow", + "cohere": "Cohere", + "huggingface": "Hugging Face", + "lingyiwanwu": "Lingyi Wanwu", + "stepfun": "StepFun", + "unknown": "Unknown" + }, + "GlobalLogs": { + "selectModel": "model Name", + "select_token_name": "token name", + "selectGroupId": "workspace", + "groupId": "Workspace", + "tokenName": "Key Name", + "channel": "Channel ID", + "keyName": "Key name" + }, + "nsManager": { + "groupId": "Workspace", + "qpm": "QPM Restrictions", + "created_at": "Creation Time", + "accessed_at": "Last use time", + "used_amount": "Amount used", + "status": "State", + "enabled": "Enabled", + "disabled": "Disabled", + "request_count": "Number of uses", + "actions": "Operation", + "ns_manager": "NS Management", + "select_group_id": "fill in the workspace", + "updateGroupStatusSuccess": "Update status successful", + "updateGroupStatusFailed": "Update status failed", + "updateGroupQpmSuccess": "Update QPM successful", + "updateGroupQpmFailed": "Update QPM failed", + "deleteGroupSuccess": "Delete successfully", + "deleteGroupFailed": "Delete failed" + }, + "channel": { + "updateSuccess": "Update status successful", + "updateFailed": "Update status failed", + "deleteSuccess": "Delete successfully", + "deleteFailed": "Delete failed" + }, + "dataDashboard": { + "title": "Dashboard", + "selectToken": "All Keys", + "selectModel": "All Models", + "day": "Last 24h", + "week": "Last 7 days", + "twoWeek": "Last 15 days", + "month": "Last 30 days", + "callCount": "Requests", + "exceptionCount": "Exceptions", + "rpm": "Current RPM", + "tpm": "Current TPM", + "cost": "Charges", + "requestData": "Call Data" + } } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index 5a6226556e44..a79ad7b4a856 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -2,12 +2,23 @@ "title": "AI 代理", "description": "AI 代理", "Sidebar": { - "Home": "API Keys", - "Logs": "调用日志", - "Price": "模型价格" + "Home": "仪表盘", + "Logs": "日志", + "Price": "模型广场", + "Dashboard": "AI 渠道", + "GlobalLogs": "全局日志", + "GlobalConfigs": "全局配置", + "NsManager": "NS管理", + "Keys": "API Keys" }, "keyList": { - "title": "API Keys" + "title": "API Keys", + "invitationText1": "限时福利!!", + "invite": "活动详情", + "invitationText2": "邀请好友注册 Sealos,", + "invitationText3": "立返 10元 余额!", + "invitationText4": "邀请链接:", + "invitationText5": "点击复制" }, "key": { "key": "API Key", @@ -29,24 +40,48 @@ "unused": "未使用", "inputPrice": "输入单价", "outputPrice": "输出单价", - "createName": "名称" + "createName": "名称", + "modelType": "模型类型", + "requestCount": "请求次数", + "usedAmount": "消耗金额", + "userNotRealName": "Ai 服务需要实名认证。" }, "logs": { "call_log": "调用日志", - "name": "名称", + "name": "Key 名称", "modal": "模型", "status": "状态", "time": "时间", "model": "模型", - "prompt_tokens": "输入", - "completion_tokens": "输出", + "prompt_tokens": "输入 Tokens", + "completion_tokens": "输出 Tokens", "price": "价格", "select_modal": "请选择模型", "select_token_name": "请选择名称", "total_price": "总金额", "total_price_tip": "(输入 token 数×输入价格)+(输出 token 数 × 输出价格)", "success": "成功", - "failed": "失败" + "failed": "失败", + "logDetail": "日志详情", + "actions": "操作", + "detail": "详情", + "requestId": "RequestID", + "mode": "模型类型", + "requestTime": "请求时间", + "totalTime": "耗时(s)", + "tokenName": "密钥名称", + "tokenId": "密钥 ID", + "info": "信息", + "requestBody": "请求内容", + "responseBody": "响应内容", + "inputTokens": "输入 Tokens", + "outputTokens": "输出 Tokens", + "searchByContent": "输入搜索关键词", + "statusOptions": { + "all": "全部", + "success": "成功", + "error": "失败" + } }, "Page": "页", "Total": "总数", @@ -73,19 +108,191 @@ "copyFailed": "复制失败", "noData": "你还没有 API Key", "price": { - "title": "模型价格", - "per1kTokens": "1k tokens" - }, - "ernie": "百度文心", - "qwen": "阿里千问", - "chatglm": "智谱", - "deepseek": "Deepseek", - "moonshot": "月之暗面", - "sparkdesk": "讯飞星火", - "abab": "Minimax", - "doubao": "字节豆包", - "glm": "智谱", - "o": "OpenAI", - "gpt": "OpenAI", - "createKey2": "新建 Key" + "all": "全部", + "title": "模型广场", + "per1kTokens": "1k tokens", + "modelOwner": "系列/厂商", + "modelType": "类型", + "modelName": "模型名", + "modelRpm": "RPM", + "modelRpmTooltip": "每个模型拥有独立的 RPM", + "sortUpTooltip": "点击升序", + "sortDownTooltip": "点击降序", + "modelVision": "视觉", + "modelVisionLabel": "该模型支持视觉能力", + "modelToolChoice": "工具调用", + "response": "输出" + }, + "createKey2": "新建 Key", + "dashboard": { + "title": "Ai 渠道", + "create": "新建", + "import": "导入", + "export": "导出", + "exportAll": "全部导出", + "importSuccess": "导入成功", + "importError": "导入失败" + }, + "channelStatus": { + "autoDisabled": "系统禁用" + }, + "channels": { + "name": "名字", + "type": "厂商", + "test": "测试", + "disable": "禁用", + "enable": "启用", + "edit": "编辑", + "export": "导出", + "create": "新建", + "name_required": "名称不合法", + "key_required": "Key 不合法", + "modelDefault": "渠道默认模型", + "createFailed": "创建渠道失败", + "createSuccess": "成功创建渠道", + "updateSuccess": "更新渠道成功", + "updateFailed": "更新渠道失败", + "id": "ID", + "requestCount": "调用次数", + "status": "状态", + "action": "操作", + "delete": "删除" + }, + "channelsForm": { + "name": "自定义名称", + "type": "厂商", + "models": "模型", + "model_mapping": "输入 & 输出模型", + "add": "添加", + "key": "密钥", + "base_url": "代理" + }, + "channelsFormPlaceholder": { + "name": "自定义名称", + "type": "Open AI", + "model": "输入选择", + "modelInput": "输入自定义模型名称", + "modelMappingInput": "输入模型", + "modelMappingOutput": "输出模型", + "key": "请输入渠道对应的鉴权密钥", + "base_url": "请输入代理" + }, + "common": { + "add": "填入" + }, + "global_configs": { + "title": "全局配置", + "export": "导出", + "import": "导入", + "qpm_limit": "全局QPM限制", + "pause_service": "暂停服务", + "retry_count": "错误重试次数", + "max_token": "工作空间最大 token 数" + }, + "globalConfigs": { + "defaultModel": "默认模型", + "addDefaultModel": "新增模型", + "saveDefaultModel": "保存", + "saveDefaultModelFailed": "保存失败", + "saveDefaultModelSuccess": "保存成功", + "saveCommonConfigSuccess": "更新成功", + "saveCommonConfigFailed": "更新失败", + "common_config": "通用配置", + "model_config": "模型设置" + }, + "modeType": { + "0": "未知", + "1": "聊天补全", + "2": "文本补全", + "3": "文本嵌入", + "4": "内容审核", + "5": "图像生成", + "6": "文本编辑", + "7": "语音合成", + "8": "语音转录", + "9": "音频翻译", + "10": "重排序" + }, + "modeOwner": { + "openai": "OpenAI", + "alibaba": "阿里", + "tencent": "腾讯", + "xunfei": "讯飞", + "deepseek": "DeepSeek", + "moonshot": "月之暗面", + "minimax": "MiniMax", + "baidu": "百度", + "google": "谷歌", + "baai": "BAAI", + "funaudiollm": "趣音大模型", + "doubao": "豆包", + "fishaudio": "鱼声科技", + "chatglm": "智谱清言", + "stabilityai": "Stability AI", + "netease": "网易", + "ai360": "360智脑", + "anthropic": "Anthropic", + "meta": "Meta", + "baichuan": "百川智能", + "mistral": "Mistral AI", + "openchat": "OpenChat", + "microsoft": "微软", + "defog": "Defog", + "nexusflow": "NexusFlow", + "cohere": "Cohere", + "huggingface": "Hugging Face", + "lingyiwanwu": "零一万物", + "stepfun": "StepFun", + "unknown": "未知" + }, + "GlobalLogs": { + "selectModel": "模型名称", + "select_token_name": "Key 名称", + "selectGroupId": "工作空间", + "groupId": "工作空间", + "tokenName": "Key 名称", + "channel": "渠道 ID", + "keyName": "Key 名称" + }, + "nsManager": { + "groupId": "工作空间", + "qpm": "QPM 限制", + "created_at": "创建时间", + "accessed_at": "最后使用时间", + "used_amount": "使用金额", + "status": "状态", + "enabled": "启用", + "disabled": "禁用", + "request_count": "使用次数", + "actions": "操作", + "ns_manager": "NS 管理", + "select_group_id": "填入工作空间", + "updateGroupStatusSuccess": "更新状态成功", + "updateGroupStatusFailed": "更新状态失败", + "updateGroupQpmSuccess": "更新 QPM 成功", + "updateGroupQpmFailed": "更新 QPM 失败", + "deleteGroupSuccess": "删除成功", + "deleteGroupFailed": "删除失败" + }, + "channel": { + "updateSuccess": "更新状态成功", + "updateFailed": "更新状态失败", + "deleteSuccess": "删除成功", + "deleteFailed": "删除失败" + }, + "dataDashboard": { + "title": "仪表盘", + "selectToken": "全部密钥", + "selectModel": "全部模型", + "day": "1天内", + "week": "近7天", + "twoWeek": "近15天", + "month": "近30天", + "callCount": "请求数", + "exceptionCount": "异常数", + "rpm": "当前 RPM", + "tpm": "当前 TPM", + "cost": "花费", + "requestData": "调用数据" + } } diff --git a/frontend/providers/aiproxy/components/InitializeApp.tsx b/frontend/providers/aiproxy/components/InitializeApp.tsx new file mode 100644 index 000000000000..ac707858581e --- /dev/null +++ b/frontend/providers/aiproxy/components/InitializeApp.tsx @@ -0,0 +1,134 @@ +'use client' + +import { EVENT_NAME } from 'sealos-desktop-sdk' +import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' +import { useCallback, useEffect } from 'react' +import { initAppConfig } from '@/api/platform' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useBackendStore } from '@/store/backend' +import { useTranslationClientSide } from '@/app/i18n/client' +import { usePathname } from 'next/navigation' +import { useRouter } from 'next/navigation' +import { useSessionStore } from '@/store/session' + +export default function InitializeApp() { + const router = useRouter() + const pathname = usePathname() + const { lng } = useI18n() + const { i18n } = useTranslationClientSide(lng) + const { + setAiproxyBackend, + setCurrencySymbol, + setDocUrl, + setIsInvitationActive, + setInvitationUrl + } = useBackendStore() + + const handleI18nChange = useCallback( + (data: { currentLanguage: string }) => { + const currentLng = i18n.resolvedLanguage // get the latest resolvedLanguage + const newLng = data.currentLanguage + + if (currentLng !== newLng) { + const currentPath = window.location.pathname + const pathWithoutLang = currentPath.split('/').slice(2).join('/') + router.push(`/${newLng}/${pathWithoutLang}`) + } + }, + [i18n.resolvedLanguage] + ) + + useEffect(() => { + const cleanupApp = createSealosApp() + let cleanupEventListener: (() => void) | undefined + + const initApp = async () => { + try { + await initLanguage() + + await initSession() + + await initConfig() + + cleanupEventListener = sealosApp?.addAppEventListen( + EVENT_NAME.CHANGE_I18N, + handleI18nChange + ) + } catch (error) { + console.error('aiproxy: init app error:', error) + } + } + + initApp() + + return () => { + if (cleanupEventListener && typeof cleanupEventListener === 'function') { + cleanupEventListener() + } + if (cleanupApp && typeof cleanupApp === 'function') { + cleanupApp() + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // init language + const initLanguage = async () => { + const pathLng = pathname.split('/')[1] + try { + const lang = await sealosApp.getLanguage() + if (pathLng !== lang.lng) { + const pathParts = pathname.split('/') + pathParts[1] = lang.lng + router.push(pathParts.join('/')) + router.refresh() + } + console.info('aiproxy: init language success') + } catch (error) { + if (error instanceof Error) { + console.debug('aiproxy: init language error:', error.message) + } else { + console.debug('aiproxy: unknown init language error:', error) + } + } + } + + // init session + const initSession = async () => { + const { setSession } = useSessionStore.getState() + + try { + const newSession = await sealosApp.getSession() + const currentSession = useSessionStore.getState().session + // Compare token from persisted session with new session token + if (newSession?.token !== currentSession?.token) { + setSession(newSession) + window.location.reload() + } + console.info('aiproxy: init session success') + } catch (err) { + console.info('aiproxy: app is not running in desktop') + if (!process.env.NEXT_PUBLIC_MOCK_USER) { + setSession(null) + } + } + } + + // init config + const initConfig = async () => { + try { + const { aiproxyBackend, currencySymbol, docUrl, isInvitationActive, invitationUrl } = + await initAppConfig() + setAiproxyBackend(aiproxyBackend) + setCurrencySymbol(currencySymbol) + setDocUrl(docUrl) + setIsInvitationActive(isInvitationActive) + setInvitationUrl(invitationUrl) + console.info('aiproxy: init config success') + } catch (error) { + console.error('aiproxy: init config error:', error) + } + } + + return null +} diff --git a/frontend/providers/aiproxy/components/MyTooltip/index.tsx b/frontend/providers/aiproxy/components/MyTooltip/index.tsx deleted file mode 100644 index dd5aefadf64a..000000000000 --- a/frontend/providers/aiproxy/components/MyTooltip/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Tooltip, TooltipProps } from '@chakra-ui/react' - -export const MyTooltip = ({ children, ...tooltipProps }: TooltipProps) => { - return ( - - {children} - - ) -} diff --git a/frontend/providers/aiproxy/components/admin/Sidebar.tsx b/frontend/providers/aiproxy/components/admin/Sidebar.tsx new file mode 100644 index 000000000000..40af5a5d297d --- /dev/null +++ b/frontend/providers/aiproxy/components/admin/Sidebar.tsx @@ -0,0 +1,121 @@ +'use client' +import { Flex, Text } from '@chakra-ui/react' +import Image, { StaticImageData } from 'next/image' +import Link from 'next/link' +import { usePathname } from 'next/navigation' + +import { useTranslationClientSide } from '@/app/i18n/client' +import homeIcon from '@/ui/svg/icons/admin-sidebar/home.svg' +import homeIcon_a from '@/ui/svg/icons/admin-sidebar/home_a.svg' +import logsIcon from '@/ui/svg/icons/admin-sidebar/logs.svg' +import logsIcon_a from '@/ui/svg/icons/admin-sidebar/logs_a.svg' +import configIcon from '@/ui/svg/icons/admin-sidebar/config.svg' +import configIcon_a from '@/ui/svg/icons/admin-sidebar/config_a.svg' +import nsManagerIcon from '@/ui/svg/icons/admin-sidebar/nsManager.svg' +import nsManagerIcon_a from '@/ui/svg/icons/admin-sidebar/nsManager_a.svg' +import { useI18n } from '@/providers/i18n/i18nContext' + +type Menu = { + id: string + url: string + value: string + icon: StaticImageData + activeIcon: StaticImageData + display: boolean +} + +const SideBar = (): JSX.Element => { + const pathname = usePathname() + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + + const menus: Menu[] = [ + { + id: 'dashboard', + url: '/dashboard', + value: t('Sidebar.Dashboard'), + icon: homeIcon, + activeIcon: homeIcon_a, + display: true + }, + { + id: 'global-logs', + url: '/global-logs', + value: t('Sidebar.GlobalLogs'), + icon: logsIcon, + activeIcon: logsIcon_a, + display: true + }, + { + id: 'global-configs', + url: '/global-configs', + value: t('Sidebar.GlobalConfigs'), + icon: configIcon, + activeIcon: configIcon_a, + display: true + }, + { + id: 'ns-manager', + url: '/ns-manager', + value: t('Sidebar.NsManager'), + icon: nsManagerIcon, + activeIcon: nsManagerIcon_a, + display: true + } + ] + + return ( + + {menus + .filter((menu) => menu.display) + .map((menu) => { + const fullUrl = `/${lng}${menu.url}` + const isActive = pathname === fullUrl + + return ( + + + {menu.value} + + {menu.value} + + + + ) + })} + + ) +} + +export default SideBar diff --git a/frontend/providers/aiproxy/components/common/ConstructMappingComponent.tsx b/frontend/providers/aiproxy/components/common/ConstructMappingComponent.tsx new file mode 100644 index 000000000000..27e61b245560 --- /dev/null +++ b/frontend/providers/aiproxy/components/common/ConstructMappingComponent.tsx @@ -0,0 +1,331 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { VStack, Flex, FormLabel, Input, Button, Text, Box } from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { CustomSelect } from './Select' +type MapKeyValuePair = { key: string; value: string } + +// mapKeys determines the available selection options +export const ConstructMappingComponent = function ({ + mapKeys, + mapData, + setMapData +}: { + mapKeys: string[] + mapData: Record + setMapData: (mapping: Record) => void +}) { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + + const [mapKeyValuePairs, setMapkeyValuePairs] = useState>([]) + + // 组件初步渲染时,isInternalUpdate 为 false,可以保证每次重新渲染时,mapData 变化时,mapKeyValuePairs 重新初始化 + const [isInternalUpdate, setIsInternalUpdate] = useState(false) + + // 初始化并在 mapData 变化时更新 mapKeyValuePairs + useEffect(() => { + if (!isInternalUpdate) { + const entries = Object.entries(mapData) + setMapkeyValuePairs( + entries.length > 0 + ? entries.map(([key, value]) => ({ key, value })) + : [{ key: '', value: '' }] + ) + } + setIsInternalUpdate(false) + }, [mapData]) + + const handleDropdownItemDisplay = (dropdownItem: string) => { + if (dropdownItem === t('channelsFormPlaceholder.modelMappingInput')) { + return ( + + {t('channelsFormPlaceholder.modelMappingInput')} + + ) + } + return ( + + {dropdownItem} + + ) + } + + const handleSeletedItemDisplay = (selectedItem: string) => { + if (selectedItem === t('channelsFormPlaceholder.modelMappingInput')) { + return ( + + {t('channelsFormPlaceholder.modelMappingInput')} + + ) + } + return ( + + + {selectedItem} + + + ) + } + + // Handling mapData and mapKeyValuePairs cleanup when map keys change. + useEffect(() => { + // 1. Handle mapData cleanup + const removedKeysFromMapData = Object.keys(mapData).filter((key) => !mapKeys.includes(key)) + if (removedKeysFromMapData.length > 0) { + const newMapData = { ...mapData } + removedKeysFromMapData.forEach((key) => { + delete newMapData[key] + }) + setIsInternalUpdate(true) + setMapData(newMapData) + } + + // 2. Handle mapKeyValuePairs cleanup + const removedPairs = mapKeyValuePairs.filter((pair) => pair.key && !mapKeys.includes(pair.key)) + if (removedPairs.length > 0) { + const newMapKeyValuePairs = mapKeyValuePairs.filter( + (pair) => !pair.key || mapKeys.includes(pair.key) + ) + setMapkeyValuePairs(newMapKeyValuePairs) + } + }, [mapKeys]) + + // Get the keys that have been selected + const getSelectedMapKeys = (currentIndex: number) => { + const selected = new Set() + mapKeyValuePairs.forEach((mapKeyValuePair, idx) => { + if (idx !== currentIndex && mapKeyValuePair.key) { + selected.add(mapKeyValuePair.key) + } + }) + return selected + } + + // Handling adding a new row + const handleAddNewMapKeyPair = () => { + setMapkeyValuePairs([...mapKeyValuePairs, { key: '', value: '' }]) + } + + // Handling deleting a row + const handleRemoveMapKeyPair = (index: number) => { + const mapKeyValuePair = mapKeyValuePairs[index] + const newMapData = { ...mapData } + if (mapKeyValuePair.key) { + delete newMapData[mapKeyValuePair.key] + } + setIsInternalUpdate(true) + setMapData(newMapData) + + const newMapKeyValuePairs = mapKeyValuePairs.filter((_, idx) => idx !== index) + setMapkeyValuePairs(newMapKeyValuePairs) + } + + // Handling selection/input changes + const handleInputChange = (index: number, field: 'key' | 'value', value: string) => { + const newMapKeyValuePairs = [...mapKeyValuePairs] + const oldValue = newMapKeyValuePairs[index][field] + newMapKeyValuePairs[index][field] = value + + // Update the mapping relationship + const newMapData = { ...mapData } + if (field === 'key') { + if (oldValue) delete newMapData[oldValue] + + if (!value) { + newMapKeyValuePairs[index].value = '' + } + + if (value && newMapKeyValuePairs[index].value) { + newMapData[value] = newMapKeyValuePairs[index].value + } + } else { + if (newMapKeyValuePairs[index].key) { + newMapData[newMapKeyValuePairs[index].key] = value + } + } + + setMapkeyValuePairs(newMapKeyValuePairs) + setIsInternalUpdate(true) + setMapData(newMapData) + } + + // Check if there are still keys that can be selected + const hasAvailableKeys = useMemo(() => { + const usedKeys = new Set( + mapKeyValuePairs.map((mapKeyValuePair) => mapKeyValuePair.key).filter(Boolean) + ) + // Ensure mapKeyValuePairs length does not exceed mapKeys length + return ( + mapKeyValuePairs.length < mapKeys.length && mapKeys.some((mapKey) => !usedKeys.has(mapKey)) + ) + }, [mapKeys, mapKeyValuePairs]) + + return ( + + + {t('channelsForm.model_mapping')} + + + {mapKeyValuePairs.map((row, index) => ( + + + listItems={mapKeys.filter((key) => !getSelectedMapKeys(index).has(key))} + initSelectedItem={row.key !== '' && row.key ? row.key : undefined} + // when select placeholder, the newSelectedItem is null + handleSelectedItemChange={(newSelectedItem) => + handleInputChange(index, 'key', newSelectedItem) + } + handleDropdownItemDisplay={handleDropdownItemDisplay} + handleSelectedItemDisplay={handleSeletedItemDisplay} + placeholder={t('channelsFormPlaceholder.modelMappingInput')} + /> + + handleInputChange(index, 'value', e.target.value)} + placeholder={t('channelsFormPlaceholder.modelMappingOutput')} + py="8px" + px="12px" + borderRadius="6px" + border="1px solid var(--Gray-Modern-200, #E8EBF0)" + bgColor="white" + sx={{ + '&::placeholder': { + color: 'grayModern.500', + fontFamily: '"PingFang SC"', + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '16px', + letterSpacing: '0.048px' + } + }} + /> + + + + ))} + + {hasAvailableKeys && ( + + )} + + ) +} +export default ConstructMappingComponent diff --git a/frontend/providers/aiproxy/components/common/ConstructModeMappingComponent.tsx b/frontend/providers/aiproxy/components/common/ConstructModeMappingComponent.tsx new file mode 100644 index 000000000000..76e8138ee0bc --- /dev/null +++ b/frontend/providers/aiproxy/components/common/ConstructModeMappingComponent.tsx @@ -0,0 +1,475 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { Flex, FormLabel, Input, Button, Text, Box, Badge, VStack } from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { CustomSelect } from './Select' +type MapKeyValuePair = { key: string; value: string } + +type Model = { + name: string + isDefault: boolean +} + +// mapKeys determines the available selection options +export const ConstructModeMappingComponent = function ({ + mapKeys, + mapData, + setMapData +}: { + mapKeys: Model[] + mapData: Record + setMapData: (mapping: Record) => void +}) { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + + const [mapKeyValuePairs, setMapkeyValuePairs] = useState>([ + { key: '', value: '' } + ]) + + const [isInternalUpdate, setIsInternalUpdate] = useState(false) + + useEffect(() => { + if (!isInternalUpdate) { + const entries = Object.entries(mapData) + setMapkeyValuePairs( + entries.length > 0 + ? entries.map(([key, value]) => ({ key, value })) + : [{ key: '', value: '' }] + ) + } + setIsInternalUpdate(false) + }, [mapData]) + + const handleDropdownItemDisplay = (dropdownItem: Model | string) => { + if (dropdownItem === t('channelsFormPlaceholder.modelMappingInput')) { + return ( + + {t('channelsFormPlaceholder.modelMappingInput')} + + ) + } + + if ((dropdownItem as Model).isDefault) { + return ( + + + {(dropdownItem as Model).name} + + + + + + + + + + + ) + } + return ( + + {(dropdownItem as Model).name} + + ) + } + + const handleSeletedItemDisplay = (selectedItem: Model | string) => { + if (selectedItem === t('channelsFormPlaceholder.modelMappingInput')) { + return ( + + {t('channelsFormPlaceholder.modelMappingInput')} + + ) + } + + if ((selectedItem as Model).isDefault) { + return ( + + ) + } + return ( + + + {(selectedItem as Model).name} + + + ) + } + + // Handling mapData and mapKeyValuePairs cleanup when map keys change. + useEffect(() => { + // 1. Handle mapData cleanup + const removedKeys = Object.keys(mapData).filter( + (key) => !mapKeys.some((model) => model.name === key) + ) + if (removedKeys.length > 0) { + // If there are mappings with removed keys, delete them + const newMapData = { ...mapData } + removedKeys.forEach((key) => { + delete newMapData[key] + }) + setIsInternalUpdate(true) + setMapData(newMapData) + } + + // 2. Handle mapKeyValuePairs cleanup + const removedPairs = mapKeyValuePairs.filter( + (pair) => pair.key && !mapKeys.some((model) => model.name === pair.key) + ) + if (removedPairs.length > 0) { + const newMapKeyValuePairs = mapKeyValuePairs.filter( + (pair) => !pair.key || mapKeys.some((model) => model.name === pair.key) + ) + setMapkeyValuePairs(newMapKeyValuePairs) + } + }, [mapKeys]) + + // Get the keys that have been selected + const getSelectedMapKeys = (currentIndex: number) => { + const selected = new Set() + mapKeyValuePairs.forEach((mapKeyValuePair, idx) => { + if (idx !== currentIndex && mapKeyValuePair.key) { + selected.add(mapKeyValuePair.key) + } + }) + return selected + } + + // Handling adding a new row + const handleAddNewMapKeyPair = () => { + setMapkeyValuePairs([...mapKeyValuePairs, { key: '', value: '' }]) + } + + // Handling deleting a row + const handleRemoveMapKeyPair = (index: number) => { + const mapKeyValuePair = mapKeyValuePairs[index] + const newMapData = { ...mapData } + if (mapKeyValuePair.key) { + delete newMapData[mapKeyValuePair.key] + } + setIsInternalUpdate(true) + setMapData(newMapData) + + const newMapKeyValuePairs = mapKeyValuePairs.filter((_, idx) => idx !== index) + setMapkeyValuePairs(newMapKeyValuePairs) + } + + // Handling selection/input changes + const handleInputChange = (index: number, field: 'key' | 'value', value: string) => { + const newMapKeyValuePairs = [...mapKeyValuePairs] + const oldValue = newMapKeyValuePairs[index][field] + newMapKeyValuePairs[index][field] = value + + // Update the mapping relationship + const newMapData = { ...mapData } + if (field === 'key') { + if (oldValue) delete newMapData[oldValue] + + if (!value) { + newMapKeyValuePairs[index].value = '' + } + + if (value && newMapKeyValuePairs[index].value) { + newMapData[value] = newMapKeyValuePairs[index].value + } + } else { + if (newMapKeyValuePairs[index].key) { + newMapData[newMapKeyValuePairs[index].key] = value + } + } + + setMapkeyValuePairs(newMapKeyValuePairs) + setIsInternalUpdate(true) + setMapData(newMapData) + } + + // Check if there are still keys that can be selected + const hasAvailableKeys = useMemo(() => { + const usedKeys = new Set( + mapKeyValuePairs.map((mapKeyValuePair) => mapKeyValuePair.key).filter(Boolean) + ) + return ( + mapKeyValuePairs.length < mapKeys.length && + mapKeys.some((mapKey) => !usedKeys.has(mapKey.name)) + ) + }, [mapKeys, mapKeyValuePairs]) + + return ( + + + {t('channelsForm.model_mapping')} + + + {mapKeyValuePairs.map((row, index) => ( + + + + listItems={mapKeys.filter((model) => !getSelectedMapKeys(index).has(model.name))} + initSelectedItem={row.key ? mapKeys.find((item) => item.name === row.key) : undefined} + // when select placeholder, the newSelectedItem is null + handleSelectedItemChange={(newSelectedItem) => { + if (newSelectedItem) { + handleInputChange(index, 'key', newSelectedItem.name) + } else { + handleInputChange(index, 'key', '') + } + }} + handleDropdownItemDisplay={handleDropdownItemDisplay} + handleSelectedItemDisplay={handleSeletedItemDisplay} + placeholder={t('channelsFormPlaceholder.modelMappingInput')} + /> + + + + handleInputChange(index, 'value', e.target.value)} + placeholder={t('channelsFormPlaceholder.modelMappingOutput')} + py="8px" + px="12px" + borderRadius="6px" + border="1px solid var(--Gray-Modern-200, #E8EBF0)" + bgColor="white" + _hover={{ borderColor: 'grayModern.300' }} + _focus={{ borderColor: 'grayModern.300' }} + _focusVisible={{ borderColor: 'grayModern.300' }} + _active={{ borderColor: 'grayModern.300' }} + sx={{ + '&::placeholder': { + color: 'grayModern.500', + fontFamily: '"PingFang SC"', + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '16px', + letterSpacing: '0.048px' + } + }} + /> + + + + + ))} + + {hasAvailableKeys && ( + + )} + + ) +} +export default ConstructModeMappingComponent diff --git a/frontend/providers/aiproxy/components/common/EditableTextNoLable.tsx b/frontend/providers/aiproxy/components/common/EditableTextNoLable.tsx new file mode 100644 index 000000000000..0f9df6ff5254 --- /dev/null +++ b/frontend/providers/aiproxy/components/common/EditableTextNoLable.tsx @@ -0,0 +1,172 @@ +'use client' +import React, { useState } from 'react' +import { + Flex, + Text, + Button, + Input, + useDisclosure, + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, + HStack, + FlexProps, + Box +} from '@chakra-ui/react' +import { CheckIcon, CloseIcon } from '@chakra-ui/icons' + +interface EditableTextProps { + value: string | number + onSubmit: (value: string) => void + flexProps?: FlexProps +} + +export const EditableTextNoLable = ({ value, onSubmit, flexProps }: EditableTextProps) => { + const [editValue, setEditValue] = useState(String(value)) + const { isOpen, onOpen, onClose } = useDisclosure() + + const handleSubmit = () => { + onSubmit(editValue) + onClose() + } + + const handleCancel = () => { + // 关闭时 恢复到传递来的初始值 + setEditValue(String(value)) + onClose() + } + + return ( + + + + + {value} + + + + + + + + setEditValue(e.target.value)} + minW="0" + w="full" + h="28px" + borderRadius="6px" + border="1px solid var(--Gray-Modern-200, #E8EBF0)" + bgColor="white" + _hover={{ borderColor: 'grayModern.300' }} + _focus={{ borderColor: 'grayModern.300' }} + _focusVisible={{ borderColor: 'grayModern.300' }} + _active={{ borderColor: 'grayModern.300' }} + autoFocus + /> + + + + + + + + + ) +} diff --git a/frontend/providers/aiproxy/components/common/MultiSelectCombobox.tsx b/frontend/providers/aiproxy/components/common/MultiSelectCombobox.tsx new file mode 100644 index 000000000000..c0b912ae73a6 --- /dev/null +++ b/frontend/providers/aiproxy/components/common/MultiSelectCombobox.tsx @@ -0,0 +1,405 @@ +'use client' +import { + Box, + Button, + Flex, + Text, + InputGroup, + Input, + FormLabel, + VStack, + ListItem, + List +} from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useState, useMemo, Dispatch, SetStateAction, ReactNode } from 'react' +import { useCombobox, useMultipleSelection } from 'downshift' + +export const MultiSelectCombobox = function ({ + dropdownItems, + selectedItems, + setSelectedItems, + handleFilteredDropdownItems, + handleDropdownItemDisplay, + handleSelectedItemDisplay, + handleSetCustomSelectedItem +}: { + dropdownItems: T[] + selectedItems: T[] + setSelectedItems: Dispatch> + handleFilteredDropdownItems: (dropdownItems: T[], selectedItems: T[], inputValue: string) => T[] + handleDropdownItemDisplay: (dropdownItem: T) => ReactNode + handleSelectedItemDisplay: (selectedItem: T) => ReactNode + handleSetCustomSelectedItem?: ( + selectedItems: T[], + setSelectedItems: Dispatch>, + customSelectedItemName: string, + setCustomSelectedItemName: Dispatch> + ) => void +}): JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + + const [inputValue, setInputValue] = useState('') + const [customSelectedItemName, setCustomSelectedItemName] = useState('') + + // Dropdown list excludes already selected options and includes those matching the input. + const items = useMemo( + () => handleFilteredDropdownItems(dropdownItems, selectedItems, inputValue), + [inputValue, selectedItems, dropdownItems, handleFilteredDropdownItems] + ) + + // 对已经选中的项目 添加处理事件 + const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection({ + selectedItems, + onStateChange({ selectedItems: newSelectedItems, type }) { + switch (type) { + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: + case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: + if (newSelectedItems) { + setSelectedItems(newSelectedItems) + } + break + default: + break + } + } + }) + const { + isOpen, + getToggleButtonProps, + getLabelProps, + getMenuProps, + getInputProps, + highlightedIndex, + getItemProps, + selectedItem + } = useCombobox({ + items, + defaultHighlightedIndex: 0, // after selection, highlight the first item. + selectedItem: null, + inputValue, + stateReducer(state, actionAndChanges) { + const { changes, type } = actionAndChanges + + switch (type) { + case useCombobox.stateChangeTypes.InputKeyDownEnter: + case useCombobox.stateChangeTypes.ItemClick: + return { + ...changes, + isOpen: true, // keep the menu open after selection. + highlightedIndex: 0 // with the first option highlighted. + } + default: + return changes + } + }, + onStateChange({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) { + switch (type) { + case useCombobox.stateChangeTypes.InputKeyDownEnter: + case useCombobox.stateChangeTypes.ItemClick: + case useCombobox.stateChangeTypes.InputBlur: + if (newSelectedItem) { + setSelectedItems([...selectedItems, newSelectedItem]) + setInputValue('') + } + break + + case useCombobox.stateChangeTypes.InputChange: + setInputValue(newInputValue ?? '') + + break + default: + break + } + } + }) + + return ( + + + + + + {t('channelsForm.models')} + + + * + + + {handleSetCustomSelectedItem && ( + + setCustomSelectedItemName(e.target.value)} + /> + + + )} + + + + + {selectedItems.map((selectedItemForRender, index) => ( + + + {handleSelectedItemDisplay(selectedItemForRender)} + { + e.stopPropagation() + removeSelectedItem(selectedItemForRender) + }}> + + + + + + + ))} + + + + + + + + + + + + {isOpen && + items.map((item, index) => ( + + {handleDropdownItemDisplay(item)} + + ))} + + + ) +} diff --git a/frontend/providers/aiproxy/components/common/MyTooltip.tsx b/frontend/providers/aiproxy/components/common/MyTooltip.tsx new file mode 100644 index 000000000000..acb61dce0b09 --- /dev/null +++ b/frontend/providers/aiproxy/components/common/MyTooltip.tsx @@ -0,0 +1,52 @@ +'use client' + +import { Tooltip, TooltipProps } from '@chakra-ui/react' + +export const MyTooltip = ({ children, ...tooltipProps }: TooltipProps) => { + const tooltipStyles = { + hasArrow: true, + placement: 'bottom' as const, + bg: 'white', + color: 'grayModern.900', + fontSize: '12px', + p: '8px 12px', + borderRadius: '8px', + display: 'flex', + w: '60px', + h: '34px', + justifyContent: 'center', + alignItems: 'center', + flexShrink: 0, + fontFamily: 'PingFang SC', + fontWeight: 400, + lineHeight: '16px', + letterSpacing: '0.048px', + boxShadow: + '0px 2px 4px 0px rgba(161, 167, 179, 0.25), 0px 0px 1px 0px rgba(121, 141, 159, 0.25)', + border: '1px solid #FFF', + sx: { + // CSS 变量定义 + '--tooltip-bg': 'white', + '--popper-arrow-bg': 'white', + '--popper-arrow-shadow-color': 'rgba(161, 167, 179, 0.25)', + '--md': '8px', + + // 箭头样式 + '& [data-popper-arrow]': { + '--popper-arrow-shadow-color': 'rgba(161, 167, 179, 0.25)', + '&::before': { + boxShadow: + '0px 2px 4px 0px rgba(161, 167, 179, 0.25), 0px 0px 1px 0px rgba(121, 141, 159, 0.25)', + border: '1px solid #FFF', + borderRadius: 'var(--md)' + } + } + } + } + + return ( + + {children} + + ) +} diff --git a/frontend/providers/aiproxy/components/common/Select.tsx b/frontend/providers/aiproxy/components/common/Select.tsx new file mode 100644 index 000000000000..9f17d6d6c5e8 --- /dev/null +++ b/frontend/providers/aiproxy/components/common/Select.tsx @@ -0,0 +1,141 @@ +'use client' +import { Box, Text, ListItem, List } from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { ReactNode } from 'react' +import { useSelect } from 'downshift' + +export const CustomSelect = function ({ + listItems, + handleSelectedItemChange, + handleDropdownItemDisplay, + handleSelectedItemDisplay, + placeholder, + initSelectedItem +}: { + listItems: T[] + handleSelectedItemChange: (selectedItem: T) => void + handleDropdownItemDisplay: (dropdownItem: T) => ReactNode + handleSelectedItemDisplay: (selectedItem: T) => ReactNode + placeholder?: string + initSelectedItem?: T +}) { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const items = [placeholder, ...listItems] + + const { + isOpen, + selectedItem, + getToggleButtonProps, + getMenuProps, + getItemProps, + highlightedIndex + } = useSelect({ + items: items, + initialSelectedItem: initSelectedItem, + onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { + if (newSelectedItem === placeholder) { + handleSelectedItemChange(undefined as T) + } else { + handleSelectedItemChange(newSelectedItem as T) + } + } + }) + + return ( + + + {selectedItem ? ( + handleSelectedItemDisplay(selectedItem as T) + ) : placeholder ? ( + + {placeholder} + + ) : ( + + Select + + )} + + + + + + + + + {isOpen && + items.map((item, index) => ( + + {handleDropdownItemDisplay(item as T)} + + ))} + + + ) +} diff --git a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx b/frontend/providers/aiproxy/components/common/SelectDateRange.tsx similarity index 98% rename from frontend/providers/aiproxy/components/SelectDateRange/index.tsx rename to frontend/providers/aiproxy/components/common/SelectDateRange.tsx index 67862c57cf29..05d6e862941f 100644 --- a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx +++ b/frontend/providers/aiproxy/components/common/SelectDateRange.tsx @@ -4,6 +4,7 @@ import { Box, Button, Flex, + FlexProps, Icon, Input, Popover, @@ -25,8 +26,9 @@ export default function SelectDateRange({ startTime, setStartTime, endTime, - setEndTime -}: SelectDateRangeProps): JSX.Element { + setEndTime, + ...props +}: SelectDateRangeProps & FlexProps): JSX.Element { const initState = { from: startTime, to: endTime } const [selectedRange, setSelectedRange] = useState(initState) @@ -180,7 +182,8 @@ export default function SelectDateRange({ boxSizing={'border-box'} justify={'space-between'} border={'1px solid #DEE0E2'} - borderRadius="6px"> + borderRadius="6px" + {...props}> + + + + {isComboboxOpen && + getFilteredDropdownItems.map((item, index) => ( + + {handleDropdownItemDisplay(item)} + + ))} + + + ) +} diff --git a/frontend/providers/aiproxy/components/common/SingleSelectComboboxUnStyle.tsx b/frontend/providers/aiproxy/components/common/SingleSelectComboboxUnStyle.tsx new file mode 100644 index 000000000000..c742a769dc1a --- /dev/null +++ b/frontend/providers/aiproxy/components/common/SingleSelectComboboxUnStyle.tsx @@ -0,0 +1,193 @@ +'use client' +import { Button, InputGroup, Input, ListItem, List, Flex, FlexProps, Box } from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useState, ReactNode, useEffect } from 'react' +import { useCombobox, UseComboboxReturnValue } from 'downshift' + +export const SingleSelectComboboxUnstyle: (props: { + dropdownItems: T[] + setSelectedItem: (value: T) => void + handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[] + handleDropdownItemDisplay: (dropdownItem: T) => ReactNode + handleInputDisplay?: (item: T) => string + initSelectedItem?: T + flexProps?: FlexProps + placeholder?: string +}) => JSX.Element = function ({ + dropdownItems, + setSelectedItem, + handleDropdownItemFilter, + handleDropdownItemDisplay, + handleInputDisplay, + initSelectedItem, + flexProps, + placeholder +}: { + dropdownItems: T[] + setSelectedItem: (value: T) => void + handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[] + handleDropdownItemDisplay: (dropdownItem: T) => ReactNode + handleInputDisplay?: (item: T) => string + initSelectedItem?: T + flexProps?: FlexProps + placeholder?: string +}) { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const [getFilteredDropdownItems, setGetFilteredDropdownItems] = useState(dropdownItems) + useEffect(() => { + setGetFilteredDropdownItems(dropdownItems) + }, [dropdownItems]) + + const { + isOpen: isComboboxOpen, + getToggleButtonProps, + getMenuProps, + getInputProps, + highlightedIndex, + getItemProps, + selectedItem + }: UseComboboxReturnValue = useCombobox({ + items: getFilteredDropdownItems, + onInputValueChange: ({ inputValue }) => { + setGetFilteredDropdownItems(handleDropdownItemFilter(dropdownItems, inputValue)) + }, + initialSelectedItem: initSelectedItem || undefined, + + itemToString: (item) => { + if (!item) return '' + return handleInputDisplay ? handleInputDisplay(item) : String(item) + }, + + onSelectedItemChange: ({ selectedItem }) => { + const selectedDropdownItem = dropdownItems.find((item) => item === selectedItem) + if (selectedDropdownItem) { + setSelectedItem(selectedDropdownItem) + } + } + }) + + return ( + + + + + + + {isComboboxOpen && + getFilteredDropdownItems.map((item, index) => ( + + {handleDropdownItemDisplay(item)} + + ))} + + + ) +} diff --git a/frontend/providers/aiproxy/components/SwitchPage.tsx b/frontend/providers/aiproxy/components/common/SwitchPage.tsx similarity index 100% rename from frontend/providers/aiproxy/components/SwitchPage.tsx rename to frontend/providers/aiproxy/components/common/SwitchPage.tsx diff --git a/frontend/providers/aiproxy/components/table/baseTable.tsx b/frontend/providers/aiproxy/components/table/BaseTable.tsx similarity index 95% rename from frontend/providers/aiproxy/components/table/baseTable.tsx rename to frontend/providers/aiproxy/components/table/BaseTable.tsx index 21f569a6d953..7e38e398c9cf 100644 --- a/frontend/providers/aiproxy/components/table/baseTable.tsx +++ b/frontend/providers/aiproxy/components/table/BaseTable.tsx @@ -16,7 +16,7 @@ export function BaseTable({ isLoading }: { table: ReactTable; isLoading: boolean } & TableContainerProps) { return ( - + {table.getHeaderGroups().map((headers) => { @@ -30,6 +30,7 @@ export function BaseTable({ key={header.id} color="grayModern.600" border="none" + textTransform="none" borderTopLeftRadius={i === 0 ? '6px' : '0'} borderBottomLeftRadius={i === 0 ? '6px' : '0'} borderTopRightRadius={i === headers.headers.length - 1 ? '6px' : '0'} diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 09b7b5e9c7d6..c79a6006a038 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -26,8 +26,10 @@ import { Input, FormErrorMessage, useDisclosure, - Center + Center, + Spinner } from '@chakra-ui/react' +import { CurrencySymbol } from '@sealos/ui' import { Column, createColumnHelper, @@ -36,17 +38,18 @@ import { useReactTable } from '@tanstack/react-table' import { TFunction } from 'i18next' -import { createKey, deleteKey, getKeys, updateKey } from '@/api/platform' +import { createToken, deleteToken, getTokens, updateToken } from '@/api/platform' import { useTranslationClientSide } from '@/app/i18n/client' import { useI18n } from '@/providers/i18n/i18nContext' -import { ChainIcon } from '@/ui/icons/home/Icons' +import { ChainIcon } from '@/ui/icons/index' import { useMessage } from '@sealos/ui' import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query' -import { TokenInfo } from '@/types/getKeys' -import SwitchPage from '@/components/SwitchPage' +import { TokenInfo } from '@/types/user/token' +import SwitchPage from '@/components/common/SwitchPage' import { useBackendStore } from '@/store/backend' -import { MyTooltip } from '@/components/MyTooltip' +import { MyTooltip } from '@/components/common/MyTooltip' +import { QueryKey } from '@/types/query-key' export function KeyList(): JSX.Element { const { lng } = useI18n() @@ -55,6 +58,7 @@ export function KeyList(): JSX.Element { return ( <> + {/* gap is 13px */} - - {/* table */} - - {/* modal */} - - + {/* table */} + + {/* modal */} + ) } @@ -84,8 +86,11 @@ export enum TableHeaderId { CREATED_AT = 'key.createdAt', LAST_USED_AT = 'key.lastUsedAt', STATUS = 'key.status', - ACTIONS = 'key.actions' + ACTIONS = 'key.actions', + REQUEST_COUNT = 'key.requestCount', + USED_AMOUNT = 'key.usedAmount' } + enum KeyStatus { ENABLED = 1, DISABLED = 2, @@ -94,6 +99,23 @@ enum KeyStatus { } const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { + const { currencySymbol } = useBackendStore() + if (column.id === TableHeaderId.USED_AMOUNT) { + return ( + + + {t(column.id as TableHeaderId)} + + + + ) + } return ( void }) => { }) const queryClient = useQueryClient() - const deleteKeyMutation = useMutation((id: number) => deleteKey(id), { + const { data, isLoading } = useQuery({ + queryKey: [QueryKey.GetTokens, page, pageSize], + queryFn: () => getTokens({ page, perPage: pageSize }), + refetchOnReconnect: true, + onSuccess(data) { + setTotal(data?.total || 0) + } + }) + + const deleteKeyMutation = useMutation((id: number) => deleteToken(id), { onSuccess() { - queryClient.invalidateQueries(['getKeys']) + queryClient.invalidateQueries([QueryKey.GetTokens]) message({ status: 'success', title: t('key.deleteSuccess'), @@ -147,10 +178,10 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { }) const updateKeyMutation = useMutation( - ({ id, status }: { id: number; status: number }) => updateKey(id, status), + ({ id, status }: { id: number; status: number }) => updateToken(id, status), { onSuccess() { - queryClient.invalidateQueries(['getKeys']) + queryClient.invalidateQueries([QueryKey.GetTokens]) message({ status: 'success', title: t('key.updateSuccess'), @@ -163,7 +194,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { message({ status: 'warning', title: t('key.updateFailed'), - description: err?.message || t('key.updateFailed'), + description: err?.message ? t(err.message) : t('key.updateFailed'), isClosable: true, position: 'top' }) @@ -181,15 +212,6 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { deleteKeyMutation.mutate(id) } - const { data, isLoading } = useQuery({ - queryKey: ['getKeys', page, pageSize], - queryFn: () => getKeys({ page, perPage: pageSize }), - refetchOnReconnect: true, - onSuccess(data) { - setTotal(data.total) - } - }) - const columnHelper = createColumnHelper() const columns = [ @@ -244,9 +266,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { ) }}> - {'sk-' + - info.getValue().substring(0, 8) + - '*'.repeat(Math.max(0, info.getValue().length - 8))} + {'sk-' + info.getValue().substring(0, 8) + '*'.repeat(3)} ) @@ -343,6 +363,46 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { } }), + columnHelper.accessor((row) => row.request_count, { + id: TableHeaderId.REQUEST_COUNT, + header: (props) => , + cell: (info) => ( + + {info.getValue()} + + ) + }), + + columnHelper.accessor((row) => row.used_amount, { + id: TableHeaderId.USED_AMOUNT, + header: (props) => , + cell: (info) => { + const value = Number(info.getValue()) + // 获取小数部分的长度 + const decimalLength = value.toString().split('.')[1]?.length || 0 + // 如果小数位超过6位则保留6位,否则保持原样 + const formattedValue = decimalLength > 6 ? value.toFixed(6) : value + + return ( + + {formattedValue} + + ) + } + }), + columnHelper.display({ id: TableHeaderId.ACTIONS, header: (props) => , @@ -354,11 +414,13 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { setOpenPopoverId(info.row.original.id)}> void }) => { display="flex" padding="6px 4px" alignItems="center" - gap="2px" + gap="8px" alignSelf="stretch" borderRadius="4px" background="transparent" @@ -406,26 +468,32 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { background: 'rgba(17, 24, 36, 0.05)', color: 'brightBlue.600' }} - leftIcon={ - - - - } onClick={() => { handleStatusUpdate(info.row.original.id, info.row.original.status) setOpenPopoverId(null) }}> - {t('enable')} + + + + + {t('enable')} + ) : ( )} @@ -525,106 +605,109 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { return ( <> - {isLoading || data?.tokens.length === 0 ? ( - - -
- - - - - - - - - - - - - - - - - {t('noData')} - - - - + + + + {t('noData')} + -
-
+ + + + ) : ( - <> + + {/* header */} @@ -634,119 +717,138 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { fontSize="14px" fontWeight={500} lineHeight="20px" + whiteSpace="nowrap" letterSpacing="0.1px"> API Endpoint: - { - const endpoint = aiproxyBackend - navigator.clipboard.writeText(endpoint).then( - () => { - message({ - status: 'success', - title: t('copySuccess'), - isClosable: true, - duration: 2000, - position: 'top' - }) - }, - (err) => { - message({ - status: 'warning', - title: t('copyFailed'), - description: err?.message || t('copyFailed'), - isClosable: true, - position: 'top' - }) - } - ) - }}> - {aiproxyBackend} - + + { + const endpoint = aiproxyBackend + navigator.clipboard.writeText(endpoint).then( + () => { + message({ + status: 'success', + title: t('copySuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + (err) => { + message({ + status: 'warning', + title: t('copyFailed'), + description: err?.message || t('copyFailed'), + isClosable: true, + position: 'top' + }) + } + ) + }}> + {aiproxyBackend} + + + {/* header end */} - - -
+ {/* table */} + + +
- {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header, i) => ( - - ))} - - ))} + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header, i) => { + return ( + + ) + })} + + ) + })} - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} + {table.getRowModel().rows.map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ) + })} + + ) + })}
- {flexRender(header.column.columnDef.header, header.getContext())} -
+ {flexRender(header.column.columnDef.header, header.getContext())} +
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
void }) => { setCurrentPage={(idx: number) => setPage(idx)} /> - + {/* table end */} +
)} ) @@ -784,11 +887,11 @@ function CreateKeyModal({ successIconFill: 'white' }) - const createKeyMutation = useMutation((name: string) => createKey(name), { + const createKeyMutation = useMutation((name: string) => createToken(name), { onSuccess(data) { createKeyMutation.reset() setName('') - queryClient.invalidateQueries(['getKeys']) + queryClient.invalidateQueries([QueryKey.GetTokens]) message({ status: 'success', title: t('key.createSuccess'), @@ -798,11 +901,12 @@ function CreateKeyModal({ }) onClose() }, - onError(err: any) { + onError(err) { + console.error(err) message({ status: 'warning', title: t('key.createFailed'), - description: err?.response?.data?.message || t('key.createFailed'), + description: err instanceof Error ? t(err.message as any) : t('key.createFailed'), isClosable: true, position: 'top' }) diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx deleted file mode 100644 index e58eca707394..000000000000 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client' -import { Badge, Center, Flex, Spinner, Text } from '@chakra-ui/react' -import { ListIcon } from '@/ui/icons/home/Icons' -import { useTranslationClientSide } from '@/app/i18n/client' -import { useI18n } from '@/providers/i18n/i18nContext' -import Image, { StaticImageData } from 'next/image' -import { useQuery } from '@tanstack/react-query' -import { getModels } from '@/api/platform' -import { useMessage } from '@sealos/ui' -// icons -import OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' -import QwenIcon from '@/ui/svg/icons/modelist/qianwen.svg' -import ChatglmIcon from '@/ui/svg/icons/modelist/chatglm.svg' -import DeepseekIcon from '@/ui/svg/icons/modelist/deepseek.svg' -import MoonshotIcon from '@/ui/svg/icons/modelist/moonshot.svg' -import SparkdeskIcon from '@/ui/svg/icons/modelist/sparkdesk.svg' -import AbabIcon from '@/ui/svg/icons/modelist/minimax.svg' -import DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg' -import ErnieIcon from '@/ui/svg/icons/modelist/ernie.svg' -import { useMemo } from 'react' -import { MyTooltip } from '@/components/MyTooltip' -import { ModelIdentifier } from '@/types/front' - -const getIdentifier = (modelName: string): ModelIdentifier => { - return modelName.toLowerCase().split(/[-._\d]/)[0] as ModelIdentifier -} - -const sortModels = (models: string[]): string[] => { - // group by identifier - const groupMap = new Map() - - // group by identifier - models.forEach((model) => { - const identifier = getIdentifier(model) - // special handle gpt and o1, group them as 'openai' - const groupKey = identifier === 'gpt' || identifier === 'o' ? 'openai' : identifier - if (!groupMap.has(groupKey)) { - groupMap.set(groupKey, []) - } - groupMap.get(groupKey)?.push(model) - }) - - // sort by identifier and flatten the result - return Array.from(groupMap.entries()) - .sort((a, b) => a[0].localeCompare(b[0])) // sort by identifier - .flatMap(([_, models]) => models.sort()) // flatten and keep the order in each group -} - -const ModelComponent = ({ modelName }: { modelName: string }) => { - const modelGroups = { - openai: { - icon: OpenAIIcon, - identifiers: ['gpt', 'o1'] - }, - ernie: { - icon: ErnieIcon, - identifiers: ['ernie'] - }, - qwen: { - icon: QwenIcon, - identifiers: ['qwen'] - }, - chatglm: { - icon: ChatglmIcon, - identifiers: ['chatglm', 'glm'] - }, - deepseek: { - icon: DeepseekIcon, - identifiers: ['deepseek'] - }, - moonshot: { - icon: MoonshotIcon, - identifiers: ['moonshot'] - }, - sparkdesk: { - icon: SparkdeskIcon, - identifiers: ['sparkdesk'] - }, - abab: { - icon: AbabIcon, - identifiers: ['abab'] - }, - doubao: { - icon: DoubaoIcon, - identifiers: ['doubao'] - } - } - - // get model icon - const getModelIcon = (modelName: string): StaticImageData => { - const identifier = getIdentifier(modelName) - const group = Object.values(modelGroups).find((group) => group.identifiers.includes(identifier)) - return group?.icon || OpenAIIcon - } - - const { lng } = useI18n() - const { t } = useTranslationClientSide(lng, 'common') - const iconSrc = getModelIcon(modelName) - const { message } = useMessage({ - warningBoxBg: 'var(--Yellow-50, #FFFAEB)', - warningIconBg: 'var(--Yellow-500, #F79009)', - warningIconFill: 'white', - successBoxBg: 'var(--Green-50, #EDFBF3)', - successIconBg: 'var(--Green-600, #039855)', - successIconFill: 'white' - }) - - return ( - - {modelName} - - - navigator.clipboard.writeText(modelName).then( - () => { - message({ - status: 'success', - title: t('copySuccess'), - isClosable: true, - duration: 2000, - position: 'top' - }) - }, - (err) => { - message({ - status: 'warning', - title: t('copyFailed'), - description: err?.message || t('copyFailed'), - isClosable: true, - position: 'top' - }) - } - ) - } - cursor="pointer" - _hover={{ color: 'blue.500' }} - transition="color 0.2s ease"> - {modelName} - - - - ) -} - -const ModelList: React.FC = () => { - const { lng } = useI18n() - const { t } = useTranslationClientSide(lng, 'common') - const { isLoading, data } = useQuery(['getModels'], () => getModels()) - - const sortedData = useMemo(() => sortModels(data || []), [data]) - - return ( - <> - - - - - {t('modelList.title')} - - - - {data?.length || 0} - - - - - - {isLoading ? ( -
- -
- ) : ( - sortedData.map((model) => ) - )} -
- - ) -} - -export default ModelList diff --git a/frontend/providers/aiproxy/components/user/Sidebar.tsx b/frontend/providers/aiproxy/components/user/Sidebar.tsx index 8b2b211db30f..698b64cec59c 100644 --- a/frontend/providers/aiproxy/components/user/Sidebar.tsx +++ b/frontend/providers/aiproxy/components/user/Sidebar.tsx @@ -11,6 +11,8 @@ import logsIcon from '@/ui/svg/icons/sidebar/logs.svg' import logsIcon_a from '@/ui/svg/icons/sidebar/logs_a.svg' import priceIcon from '@/ui/svg/icons/sidebar/price.svg' import priceIcon_a from '@/ui/svg/icons/sidebar/price_a.svg' +import keysIcon from '@/ui/svg/icons/sidebar/key.svg' +import keysIcon_a from '@/ui/svg/icons/sidebar/key_a.svg' import { useI18n } from '@/providers/i18n/i18nContext' type Menu = { @@ -28,6 +30,14 @@ const SideBar = (): JSX.Element => { const { t } = useTranslationClientSide(lng, 'common') const menus: Menu[] = [ + { + id: 'keys', + url: '/key', + value: t('Sidebar.Keys'), + icon: keysIcon, + activeIcon: keysIcon_a, + display: true + }, { id: 'home', url: '/home', @@ -38,7 +48,7 @@ const SideBar = (): JSX.Element => { }, { id: 'logs', - url: '/logs', + url: '/log', value: t('Sidebar.Logs'), icon: logsIcon, activeIcon: logsIcon_a, @@ -61,7 +71,7 @@ const SideBar = (): JSX.Element => { px="12px" gap="var(--md, 8px)" alignContent="center" - flexShrink={0}> + flex="1"> {menus .filter((menu) => menu.display) .map((menu) => { diff --git a/frontend/providers/aiproxy/hooks/useDebounce.ts b/frontend/providers/aiproxy/hooks/useDebounce.ts new file mode 100644 index 000000000000..aa770c4da03a --- /dev/null +++ b/frontend/providers/aiproxy/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue +} diff --git a/frontend/providers/aiproxy/package.json b/frontend/providers/aiproxy/package.json index d4e321884ec5..33b1952a9b31 100644 --- a/frontend/providers/aiproxy/package.json +++ b/frontend/providers/aiproxy/package.json @@ -9,12 +9,15 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@sealos/ui": "workspace:*", "@tanstack/react-query": "^4.35.3", "@tanstack/react-table": "^8.10.7", "accept-language": "^3.0.20", "axios": "^1.7.7", "date-fns": "^2.30.0", + "downshift": "^9.0.8", + "echarts": "^5.4.3", "i18next": "^23.11.5", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.6.2", @@ -26,7 +29,9 @@ "react-day-picker": "^8.8.2", "react-dom": "^18", "react-hook-form": "^7.46.2", + "react-json-view": "^1.21.3", "sealos-desktop-sdk": "workspace:*", + "zod": "^3.23.8", "zustand": "^4.5.4" }, "devDependencies": { @@ -42,4 +47,4 @@ "tailwindcss": "^3.4.1", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/frontend/providers/aiproxy/providers/chakra/QueryProvider.tsx b/frontend/providers/aiproxy/providers/tanstack-query/QueryProvider.tsx similarity index 100% rename from frontend/providers/aiproxy/providers/chakra/QueryProvider.tsx rename to frontend/providers/aiproxy/providers/tanstack-query/QueryProvider.tsx diff --git a/frontend/providers/aiproxy/store/backend.ts b/frontend/providers/aiproxy/store/backend.ts index de6bec27dae4..6c92761ebba5 100644 --- a/frontend/providers/aiproxy/store/backend.ts +++ b/frontend/providers/aiproxy/store/backend.ts @@ -4,8 +4,14 @@ import { persist } from 'zustand/middleware' interface BackendState { aiproxyBackend: string currencySymbol: 'shellCoin' | 'usd' | 'cny' + docUrl: string + invitationUrl: string + isInvitationActive: boolean setAiproxyBackend: (backend: string) => void setCurrencySymbol: (symbol: 'shellCoin' | 'usd' | 'cny') => void + setDocUrl: (url: string) => void + setIsInvitationActive: (active: boolean) => void + setInvitationUrl: (url: string) => void } export const useBackendStore = create()( @@ -13,8 +19,14 @@ export const useBackendStore = create()( (set) => ({ aiproxyBackend: '', currencySymbol: 'shellCoin', + docUrl: '', + invitationUrl: '', + isInvitationActive: false, setAiproxyBackend: (backend) => set({ aiproxyBackend: backend }), - setCurrencySymbol: (symbol) => set({ currencySymbol: symbol }) + setCurrencySymbol: (symbol) => set({ currencySymbol: symbol }), + setDocUrl: (url) => set({ docUrl: url }), + setIsInvitationActive: (active) => set({ isInvitationActive: active }), + setInvitationUrl: (url) => set({ invitationUrl: url }) }), { name: 'aiproxy-backend-storage' diff --git a/frontend/providers/aiproxy/store/session.ts b/frontend/providers/aiproxy/store/session.ts new file mode 100644 index 000000000000..90f7d5214614 --- /dev/null +++ b/frontend/providers/aiproxy/store/session.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' +import { immer } from 'zustand/middleware/immer' +import { SessionV1 } from 'sealos-desktop-sdk' + +interface SessionState { + session: SessionV1 | null + setSession: (session: SessionV1 | null) => void +} + +export const useSessionStore = create()( + persist( + immer((set) => ({ + session: null as SessionV1 | null, + setSession: (session) => + set((state) => { + state.session = session + }) + })), + { + name: 'session' + } + ) +) diff --git a/frontend/providers/aiproxy/types/admin/channels/channelInfo.ts b/frontend/providers/aiproxy/types/admin/channels/channelInfo.ts new file mode 100644 index 000000000000..80c08193fba2 --- /dev/null +++ b/frontend/providers/aiproxy/types/admin/channels/channelInfo.ts @@ -0,0 +1,43 @@ +export interface ChannelInfo { + model_mapping: Record + config: Record + other: string + key: string + name: string + base_url: string + models: any[] + balance: number + response_duration: number + id: number + used_amount: number + request_count: number + status: number + type: number + priority: number + created_at: number + accessed_at: number + test_at: number + balance_updated_at: number +} + +export type CreateChannelRequest = { + type: number + name: string + key: string + base_url: string + models: string[] + model_mapping: Record +} + +export enum ChannelStatus { + ChannelStatusUnknown = 0, + ChannelStatusEnabled = 1, + ChannelStatusDisabled = 2, + ChannelStatusAutoDisabled = 3 +} + +export type ChannelType = `${number}` + +export type ChannelTypeMapName = { + [key in ChannelType]: string +} diff --git a/frontend/providers/aiproxy/types/admin/group.ts b/frontend/providers/aiproxy/types/admin/group.ts new file mode 100644 index 000000000000..9eb7845fd8c9 --- /dev/null +++ b/frontend/providers/aiproxy/types/admin/group.ts @@ -0,0 +1,20 @@ +export interface GroupInfo { + id: string + status: number + used_amount: number + qpm: number + request_count: number + created_at: number + accessed_at: number +} + +export interface GroupQueryParams { + keyword?: string + page: number + perPage: number +} + +export enum GroupStatus { + ENABLED = 1, + DISABLED = 2 +} diff --git a/frontend/providers/aiproxy/types/admin/option.ts b/frontend/providers/aiproxy/types/admin/option.ts new file mode 100644 index 000000000000..bd7ed0de6661 --- /dev/null +++ b/frontend/providers/aiproxy/types/admin/option.ts @@ -0,0 +1,34 @@ +import { ChannelType } from './channels/channelInfo' + +export interface BatchOptionData { + DefaultChannelModelMapping: string + DefaultChannelModels: string +} + +export interface OptionData { + ApproximateTokenEnabled: string + AutomaticDisableChannelEnabled: string + AutomaticEnableChannelWhenTestSucceedEnabled: string + BillingEnabled: string + CompletionPrice: string + DefaultChannelModelMapping: string + DefaultChannelModels: string + DefaultGroupQPM: string + DisableServe: string + GeminiSafetySetting: string + GeminiVersion: string + GlobalApiRateLimitNum: string + GroupMaxTokenNum: string + ModelPrice: string + RetryTimes: string +} + +export type DefaultChannelModel = { + [key in ChannelType]: string[] +} + +export type DefaultChannelModelMapping = { + [key in ChannelType]: { + [modelKey: string]: string + } +} diff --git a/frontend/providers/aiproxy/types/api.d.ts b/frontend/providers/aiproxy/types/api.d.ts index 71452bae2296..22e8ddff50cc 100644 --- a/frontend/providers/aiproxy/types/api.d.ts +++ b/frontend/providers/aiproxy/types/api.d.ts @@ -1,8 +1,23 @@ -export interface ApiResp { +export type ApiResp = { code: number + message?: string + error?: string + data?: T +} + +export type ApiProxyBackendResp = { + success: boolean message: string data?: T } -export const isApiResp = (x: unknown): x is ApiResp => - typeof x.code === 'number' && typeof x.message === 'string' +export const isApiResp = (x: unknown): x is ApiResp => { + return ( + typeof x === 'object' && + x !== null && + 'code' in x && + 'message' in x && + typeof (x as any).code === 'number' && + typeof (x as any).message === 'string' + ) +} diff --git a/frontend/providers/aiproxy/types/appConfig.d.ts b/frontend/providers/aiproxy/types/app-config.d.ts similarity index 63% rename from frontend/providers/aiproxy/types/appConfig.d.ts rename to frontend/providers/aiproxy/types/app-config.d.ts index 7ddd60466770..7597536148dc 100644 --- a/frontend/providers/aiproxy/types/appConfig.d.ts +++ b/frontend/providers/aiproxy/types/app-config.d.ts @@ -1,12 +1,20 @@ export type AppConfigType = { + common: { + docUrl: string + isInvitationActive: boolean + invitationUrl: string + } auth: { appTokenJwtKey: string aiProxyBackendKey: string + accountServerTokenJwtKey: string } backend: { aiproxy: string aiproxyInternal: string + accountServer: string } + adminNameSpace: string[] currencySymbol: 'shellCoin' | 'cny' | 'usd' } diff --git a/frontend/providers/aiproxy/types/backend.d.ts b/frontend/providers/aiproxy/types/backend.d.ts deleted file mode 100644 index d572c684e7cb..000000000000 --- a/frontend/providers/aiproxy/types/backend.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ModelPrice { - name: string - prompt: number - completion: number -} diff --git a/frontend/providers/aiproxy/types/form.d.ts b/frontend/providers/aiproxy/types/form.d.ts deleted file mode 100644 index 3f5d605bbb5c..000000000000 --- a/frontend/providers/aiproxy/types/form.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface LogForm { - name: string - modelName: string - createdAt: Date - endedAt: Date - page: number - pageSize: number -} diff --git a/frontend/providers/aiproxy/types/front.d.ts b/frontend/providers/aiproxy/types/front.d.ts index de83dc1fd21c..e69de29bb2d1 100644 --- a/frontend/providers/aiproxy/types/front.d.ts +++ b/frontend/providers/aiproxy/types/front.d.ts @@ -1,12 +0,0 @@ -export type ModelIdentifier = - | 'ernie' - | 'qwen' - | 'chatglm' - | 'gpt' // Add these - | 'o' // Add these - | 'deepseek' - | 'moonshot' - | 'sparkdesk' - | 'abab' - | 'glm' - | 'doubao' diff --git a/frontend/providers/aiproxy/types/models/model.ts b/frontend/providers/aiproxy/types/models/model.ts new file mode 100644 index 000000000000..b8085242fe38 --- /dev/null +++ b/frontend/providers/aiproxy/types/models/model.ts @@ -0,0 +1,42 @@ +import { ChannelType } from '@/types/admin/channels/channelInfo' + +export interface ModelConfig { + config?: ModelConfigDetail + created_at: number + updated_at: number + image_prices: number[] | null + model: string + owner: string + image_batch_size: number + type: number + input_price: number + output_price: number + rpm: number +} + +export type ChannelWithMode = { + [K in ChannelType]?: ModelConfig[] +} + +export type ChannelDefaultModeMapping = { + [K in ChannelType]?: { + [modelKey: string]: string + } +} + +export type ChannelDefaultModel = { + [K in ChannelType]?: string[] +} + +export type ChannelWithDefaultModelAndDefaultModeMapping = { + mapping: ChannelDefaultModeMapping + models: ChannelDefaultModel +} + +export interface ModelConfigDetail { + max_input_tokens?: number + max_output_tokens?: number + max_context_tokens?: number + vision?: boolean + tool_choice?: boolean +} diff --git a/frontend/providers/aiproxy/types/query-key.ts b/frontend/providers/aiproxy/types/query-key.ts new file mode 100644 index 000000000000..eff327f6e7c6 --- /dev/null +++ b/frontend/providers/aiproxy/types/query-key.ts @@ -0,0 +1,22 @@ +export enum QueryKey { + // 共用 + // common + GetTokens = 'getTokens', + GetUserLogs = 'getUserLogs', + GetEnabledModels = 'getEnabledModels', + GetDashboardData = 'getDashboardData', + GetUserLogDetail = 'getUserLogDetail', + + // admin + GetChannels = 'getChannels', + GetAllChannels = 'getAllChannels', + GetGlobalLogs = 'getGlobalLogs', + GetGroups = 'getGroups', + GetChannelTypeNames = 'getChannelTypeNames', + GetAllChannelModes = 'getAllChannelModes', + GetDefaultModelAndModeMapping = 'getDefaultModelAndModeMapping', + GetOption = 'getOption', + + // 组件自己管理 + GetCommonConfig = 'getCommonConfig' +} diff --git a/frontend/providers/aiproxy/types/user/dashboard.ts b/frontend/providers/aiproxy/types/user/dashboard.ts new file mode 100644 index 000000000000..fcefa76c6d61 --- /dev/null +++ b/frontend/providers/aiproxy/types/user/dashboard.ts @@ -0,0 +1,20 @@ +import { ApiResp } from '../api' + +export interface ChartDataItem { + timestamp: number + request_count: number + used_amount: number + exception_count: number +} +export interface DashboardData { + chart_data: ChartDataItem[] + token_names: string[] + models: string[] + total_count: number + exception_count: number + used_amount: number + rpm: number + tpm: number +} + +export type DashboardResponse = ApiResp diff --git a/frontend/providers/aiproxy/types/log.d.ts b/frontend/providers/aiproxy/types/user/logs.ts similarity index 53% rename from frontend/providers/aiproxy/types/log.d.ts rename to frontend/providers/aiproxy/types/user/logs.ts index 35e53134b775..cef98d63ddd5 100644 --- a/frontend/providers/aiproxy/types/log.d.ts +++ b/frontend/providers/aiproxy/types/user/logs.ts @@ -1,8 +1,20 @@ +export interface RequestDetail { + request_body?: string + response_body?: string + id: number + log_id: number +} + export interface LogItem { + request_detail?: RequestDetail + request_id: string + request_at: number + id: number code: number content: string group: string model: string + mode: number used_amount: number price: number completion_price: number @@ -15,11 +27,4 @@ export interface LogItem { created_at: number } -export interface LogResponse { - data: { - logs: LogItem[] - total: number - } - message: string - success: boolean -} +export interface GlobalLogItem extends LogItem {} diff --git a/frontend/providers/aiproxy/types/getKeys.d.ts b/frontend/providers/aiproxy/types/user/token.ts similarity index 100% rename from frontend/providers/aiproxy/types/getKeys.d.ts rename to frontend/providers/aiproxy/types/user/token.ts diff --git a/frontend/providers/aiproxy/ui/icons/home/Icons.tsx b/frontend/providers/aiproxy/ui/icons/home/Icons.tsx deleted file mode 100644 index 54c78739c00d..000000000000 --- a/frontend/providers/aiproxy/ui/icons/home/Icons.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Icon, IconProps } from '@chakra-ui/react' - -export const ChainIcon = (props: IconProps) => ( - - - -) - -export const ListIcon = (props: IconProps) => ( - - - - - - - - -) - -// 使用示例: -// diff --git a/frontend/providers/aiproxy/ui/icons/index.tsx b/frontend/providers/aiproxy/ui/icons/index.tsx index 14c182dd619b..7c01da583b2d 100644 --- a/frontend/providers/aiproxy/ui/icons/index.tsx +++ b/frontend/providers/aiproxy/ui/icons/index.tsx @@ -33,3 +33,43 @@ export function RightFirstIcon(props: IconProps) { ) } + +export const ChainIcon = (props: IconProps) => ( + + + +) + +export const ListIcon = (props: IconProps) => ( + + + + + + + + +) diff --git a/frontend/providers/aiproxy/ui/icons/mode-icons/index.tsx b/frontend/providers/aiproxy/ui/icons/mode-icons/index.tsx new file mode 100644 index 000000000000..48ff3e4fca72 --- /dev/null +++ b/frontend/providers/aiproxy/ui/icons/mode-icons/index.tsx @@ -0,0 +1,66 @@ +import OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' +import QwenIcon from '@/ui/svg/icons/modelist/qianwen.svg' +import ChatglmIcon from '@/ui/svg/icons/modelist/chatglm.svg' +import DeepseekIcon from '@/ui/svg/icons/modelist/deepseek.svg' +import MoonshotIcon from '@/ui/svg/icons/modelist/moonshot.svg' +import SparkdeskIcon from '@/ui/svg/icons/modelist/sparkdesk.svg' +import DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg' +import BaaiIcon from '@/ui/svg/icons/modelist/baai.svg' +import HunyuanIcon from '@/ui/svg/icons/modelist/hunyuan.svg' +import MiniMaxIcon from '@/ui/svg/icons/modelist/minimax.svg' +import BaiduIcon from '@/ui/svg/icons/modelist/baidu.svg' +import GoogleGeminiIcon from '@/ui/svg/icons/modelist/google.svg' +import AlibabaIcon from '@/ui/svg/icons/modelist/alibaba.svg' +import FishAudioIcon from '@/ui/svg/icons/modelist/fishaudio.svg' +import StabilityAIIcon from '@/ui/svg/icons/modelist/stabilityai.svg' +import NeteaseIcon from '@/ui/svg/icons/modelist/netease.svg' +import AI360Icon from '@/ui/svg/icons/modelist/ai360.svg' +import AnthropicIcon from '@/ui/svg/icons/modelist/anthropic.svg' +import BaichuanIcon from '@/ui/svg/icons/modelist/baichuan.svg' +import MetaIcon from '@/ui/svg/icons/modelist/meta.svg' +import MistralIcon from '@/ui/svg/icons/modelist/mistral.svg' +import OpenChatIcon from '@/ui/svg/icons/modelist/openchat.svg' +import DefogIcon from '@/ui/svg/icons/modelist/defog.svg' +import NexusFlowIcon from '@/ui/svg/icons/modelist/nexusflow.svg' +import MicrosoftIcon from '@/ui/svg/icons/modelist/microsoft.svg' +import CohereIcon from '@/ui/svg/icons/modelist/cohere.svg' +import HuggingFaceIcon from '@/ui/svg/icons/modelist/huggingface.svg' +import LingyiWanwuIcon from '@/ui/svg/icons/modelist/lingyiwanwu.svg' +import StepFunIcon from '@/ui/svg/icons/modelist/stepfun.svg' +import DefaultIcon from '@/ui/svg/icons/modelist/default.svg' + +import AbabIcon from '@/ui/svg/icons/modelist/minimax.svg' +import ErnieIcon from '@/ui/svg/icons/modelist/ernie.svg' + +export const modelIcons = { + openai: OpenAIIcon, + alibaba: QwenIcon, + tencent: HunyuanIcon, + xunfei: SparkdeskIcon, + deepseek: DeepseekIcon, + moonshot: MoonshotIcon, + minimax: MiniMaxIcon, + baidu: BaiduIcon, + google: GoogleGeminiIcon, + baai: BaaiIcon, + funaudiollm: AlibabaIcon, + doubao: DoubaoIcon, + fishaudio: FishAudioIcon, + chatglm: ChatglmIcon, + stabilityai: StabilityAIIcon, + netease: NeteaseIcon, + ai360: AI360Icon, + anthropic: AnthropicIcon, + meta: MetaIcon, + baichuan: BaichuanIcon, + mistral: MistralIcon, + openchat: OpenChatIcon, + microsoft: MicrosoftIcon, + defog: DefogIcon, + nexusflow: NexusFlowIcon, + cohere: CohereIcon, + huggingface: HuggingFaceIcon, + lingyiwanwu: LingyiWanwuIcon, + stepfun: StepFunIcon, + default: DefaultIcon +} diff --git a/frontend/providers/aiproxy/ui/icons/sidebar/HomeIcon.tsx b/frontend/providers/aiproxy/ui/icons/sidebar/HomeIcon.tsx deleted file mode 100644 index a2ddb485ae11..000000000000 --- a/frontend/providers/aiproxy/ui/icons/sidebar/HomeIcon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { createIcon } from '@chakra-ui/react'; - -export const ConsoleIcon = createIcon({ - displayName: 'ConsoleIcon', - viewBox: '0 0 24 24', - path: ( - <> - - - - - - ) -}); diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/config.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/config.svg new file mode 100644 index 000000000000..1b3f2ee5f1ec --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/config.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/config_a.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/config_a.svg new file mode 100644 index 000000000000..b9c9f721f66d --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/config_a.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/home.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/home.svg new file mode 100644 index 000000000000..9fe618a35f77 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/home.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/home_a.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/home_a.svg new file mode 100644 index 000000000000..3ba408e77868 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/home_a.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/logs.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/logs.svg new file mode 100644 index 000000000000..d40349cbd0a3 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/logs.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/logs_a.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/logs_a.svg new file mode 100644 index 000000000000..69879256e108 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/logs_a.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/nsManager.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/nsManager.svg new file mode 100644 index 000000000000..7eca78e78987 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/nsManager.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/nsManager_a.svg b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/nsManager_a.svg new file mode 100644 index 000000000000..c4a775336218 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/admin-sidebar/nsManager_a.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/ai360.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/ai360.svg new file mode 100644 index 000000000000..c0bde5157c06 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/ai360.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/alibaba.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/alibaba.svg new file mode 100644 index 000000000000..67e870a156a2 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/alibaba.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/anthropic.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/anthropic.svg new file mode 100644 index 000000000000..626429dbcb7d --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/anthropic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/baai.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/baai.svg new file mode 100644 index 000000000000..ec85f45d1509 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/baai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/baichuan.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/baichuan.svg new file mode 100644 index 000000000000..9c1ef6a9d607 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/baichuan.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/baidu.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/baidu.svg new file mode 100644 index 000000000000..c64f120991ab --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/baidu.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/cohere.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/cohere.svg new file mode 100644 index 000000000000..150bc29de8f0 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/cohere.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/default.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/default.svg new file mode 100644 index 000000000000..979bd0cbaf38 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/default.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/defog.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/defog.svg new file mode 100644 index 000000000000..0b125ba2d855 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/defog.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/fishaudio.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/fishaudio.svg new file mode 100644 index 000000000000..ec44029a9023 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/fishaudio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/google.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/google.svg new file mode 100644 index 000000000000..759184e8808f --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/google.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/huggingface.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/huggingface.svg new file mode 100644 index 000000000000..43c5d3c0c97a --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/huggingface.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/hunyuan.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/hunyuan.svg new file mode 100644 index 000000000000..d7e6fc655219 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/hunyuan.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/lingyiwanwu.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/lingyiwanwu.svg new file mode 100644 index 000000000000..2b8ad4b4c3a9 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/lingyiwanwu.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/meta.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/meta.svg new file mode 100644 index 000000000000..9390c42cb7bb --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/meta.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/microsoft.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/microsoft.svg new file mode 100644 index 000000000000..76115f3cbb1d --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/microsoft.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/mistral.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/mistral.svg new file mode 100644 index 000000000000..1680f52cb89a --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/mistral.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/netease.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/netease.svg new file mode 100644 index 000000000000..b24bd07f50a8 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/netease.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/nexusflow.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/nexusflow.svg new file mode 100644 index 000000000000..14bd9e47dfc8 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/nexusflow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/openchat.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/openchat.svg new file mode 100644 index 000000000000..f19c346008d2 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/openchat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/stabilityai.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/stabilityai.svg new file mode 100644 index 000000000000..5c87e29fc9be --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/stabilityai.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/stepfun.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/stepfun.svg new file mode 100644 index 000000000000..ec0eed0dbd17 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/stepfun.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg index 9fe618a35f77..3eb17800823a 100644 --- a/frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg @@ -1,6 +1,3 @@ - - - - + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg index 3ba408e77868..dfe212417eca 100644 --- a/frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg @@ -1,6 +1,3 @@ - - - - + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/key.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/key.svg new file mode 100644 index 000000000000..9fe618a35f77 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/key.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/key_a.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/key_a.svg new file mode 100644 index 000000000000..3ba408e77868 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/key_a.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg index 4a014d509e78..f782e2a12e31 100644 --- a/frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg @@ -1,4 +1,6 @@ - - + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg index 439cb2df3f9d..2b83d4c9ba46 100644 --- a/frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg @@ -1,4 +1,6 @@ - - + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/utils/auth.ts b/frontend/providers/aiproxy/utils/auth.ts deleted file mode 100644 index 59e9e2c0584d..000000000000 --- a/frontend/providers/aiproxy/utils/auth.ts +++ /dev/null @@ -1,39 +0,0 @@ -import jwt from 'jsonwebtoken' - -// Token payload 类型定义 -interface AppTokenPayload { - workspaceUid: string - workspaceId: string - regionUid: string - userCrUid: string - userCrName: string - userId: string - userUid: string - iat: number - exp: number -} - -export async function parseJwtToken(headers: Headers): Promise { - try { - const token = headers.get('authorization') - if (!token) { - return Promise.reject('Token is missing') - } - - const decoded = jwt.verify( - token, - global.AppConfig?.auth.appTokenJwtKey || '' - ) as AppTokenPayload - const now = Math.floor(Date.now() / 1000) - if (decoded.exp && decoded.exp < now) { - return Promise.reject('Token expired') - } - if (!decoded.workspaceId) { - return Promise.reject('Invalid token') - } - return decoded.workspaceId - } catch (error) { - console.error('Token parsing error:', error) - return Promise.reject('Invalid token') - } -} diff --git a/frontend/providers/aiproxy/utils/backend/auth.ts b/frontend/providers/aiproxy/utils/backend/auth.ts new file mode 100644 index 000000000000..a5280fd26ebe --- /dev/null +++ b/frontend/providers/aiproxy/utils/backend/auth.ts @@ -0,0 +1,115 @@ +import jwt from 'jsonwebtoken' + +// Token payload +interface AppTokenPayload { + workspaceUid: string + workspaceId: string + regionUid: string + userCrUid: string + userCrName: string + userId: string + userUid: string + iat: number + exp: number +} + +type RealNameInfoResponse = { + data: { + userID: string + isRealName: boolean + } + error?: string + message: string +} + +export async function parseJwtToken(headers: Headers): Promise { + try { + const token = headers.get('authorization') + if (!token) { + return Promise.reject('Auth: Token is missing') + } + + const decoded = jwt.verify( + token, + global.AppConfig?.auth.appTokenJwtKey || '' + ) as AppTokenPayload + const now = Math.floor(Date.now() / 1000) + if (decoded.exp && decoded.exp < now) { + return Promise.reject('Auth: Token expired') + } + if (!decoded.workspaceId) { + return Promise.reject('Auth: Invalid token') + } + return decoded.workspaceId + } catch (error) { + console.error('Auth: Token parsing error:', error) + return Promise.reject('Auth: Invalid token') + } +} + +export async function checkSealosUserIsRealName(headers: Headers): Promise { + if (!global.AppConfig?.backend.accountServer) { + console.warn('CheckSealosUserIsRealName: Account server is not set') + return true + } + + if (!global.AppConfig?.auth.accountServerTokenJwtKey) { + console.warn('CheckSealosUserIsRealName: Account server token jwt key is not set') + return true + } + + try { + const token = headers.get('authorization') + if (!token) { + console.error('CheckSealosUserIsRealName: Token is missing') + return false + } + + const decoded = jwt.verify( + token, + global.AppConfig?.auth.appTokenJwtKey || '' + ) as AppTokenPayload + const now = Math.floor(Date.now() / 1000) + if (decoded.exp && decoded.exp < now) { + console.error('CheckSealosUserIsRealName: Token expired') + return false + } + + if (!decoded.userUid && !decoded.userId) { + console.error('CheckSealosUserIsRealName: User uid or user id is missing, token is invalid') + return false + } + + const accountServerToken = jwt.sign( + { + userUid: decoded.userUid, + userId: decoded.userId + }, + global.AppConfig?.auth.accountServerTokenJwtKey, + { expiresIn: '5d' } + ) + + const response = await fetch( + `${global.AppConfig?.backend.accountServer}/account/v1alpha1/real-name-info`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${accountServerToken}`, + 'Content-Type': 'application/json', + 'Accept-Encoding': 'gzip,deflate,compress' + }, + cache: 'no-store' + } + ) + const result: RealNameInfoResponse = await response.json() + if (result.error) { + console.error(result.error) + return false + } + + return result.data.isRealName + } catch (error) { + console.error('CheckSealosUserIsRealName: ', error) + return false + } +} diff --git a/frontend/providers/aiproxy/utils/backend/isAdmin.ts b/frontend/providers/aiproxy/utils/backend/isAdmin.ts new file mode 100644 index 000000000000..0a3a054fd7fc --- /dev/null +++ b/frontend/providers/aiproxy/utils/backend/isAdmin.ts @@ -0,0 +1,14 @@ +export async function isAdmin(namespace: string): Promise { + if (!namespace) { + return Promise.reject('Admin: Invalid namespace') + } + try { + if (global.AppConfig?.adminNameSpace.includes(namespace)) { + return namespace + } + return Promise.reject('Admin: Invalid namespace') + } catch (error) { + console.error('Admin: check namespace error:', error) + return Promise.reject('Admin: Invalid namespace') + } +} diff --git a/frontend/providers/aiproxy/utils/common.ts b/frontend/providers/aiproxy/utils/common.ts new file mode 100644 index 000000000000..25743fc85663 --- /dev/null +++ b/frontend/providers/aiproxy/utils/common.ts @@ -0,0 +1,36 @@ +// 根据枚举值获取枚举键 +export const getEnumKeyByValue = ( + enumObj: T, + value: string +): keyof T | undefined => { + const keys = Object.keys(enumObj) as Array + return keys.find((key) => enumObj[key] === value) +} + +/** + * 获取翻译,如果翻译不存在则返回指定的默认翻译 + * @param key - 翻译键 + * @param defaultKey - 默认翻译键 + * @param t - i18n 翻译函数 + */ +export const getTranslationWithFallback = ( + key: string, + defaultKey: string, + t: (key: string) => string +): string => { + const translated = t(key) + return translated === key ? t(defaultKey) : translated +} + +// 下载 JSON 文件 +export const downloadJson = (data: T, filename: string): void => { + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `${filename}_${new Date().toISOString()}.json` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) +} diff --git a/frontend/providers/aiproxy/utils/frontend/request.ts b/frontend/providers/aiproxy/utils/frontend/request.ts new file mode 100644 index 000000000000..2b7561b5a1bc --- /dev/null +++ b/frontend/providers/aiproxy/utils/frontend/request.ts @@ -0,0 +1,151 @@ +import { ApiResp } from '@/types/api' +import axios, { InternalAxiosRequestConfig, AxiosResponse, AxiosRequestConfig } from 'axios' +import { getAppToken } from './user' + +const request = axios.create({ + baseURL: '/', + withCredentials: true, + timeout: 60000 +}) + +// request interceptor +request.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // auto append service prefix + if (config.url && !config.url.startsWith('/api/')) { + config.url = `/api${config.url}` + } + + // ensure headers exists + config.headers = config.headers || {} + + // append user session to Authorization header + const appToken = getAppToken() + if (appToken) { + config.headers['Authorization'] = appToken + } + + // set default Content-Type + if (!config.headers['Content-Type']) { + config.headers['Content-Type'] = 'application/json' + } + + // 如果是 FormData,删除 Content-Type,让浏览器自动设置 + if (config.data instanceof FormData) { + delete config.headers['Content-Type'] + } + + return config + }, + (error: any) => { + // handle request interceptor error + console.error('Request Interceptor Error:', error) + error.data = { + msg: 'An error occurred while making the request. Please try again later.' + } + return Promise.reject(error) // use reject to catch error in subsequent process + } +) + +request.interceptors.response.use( + (response: AxiosResponse) => { + // only process status code 200 + const { data } = response.data + return data + }, + (error: any) => { + if (axios.isCancel(error)) { + return Promise.reject(new Error(`cancel request: ${error.message || error}`)) + } + + const apiResponse = error?.response?.data as ApiResp + if (apiResponse?.error || apiResponse?.message) { + error.message = apiResponse.error || apiResponse.message + } else { + error.message = 'An unknown error occurred. Please try again later.' + } + + return Promise.reject(error) + } +) + +/** + * GET request + * @param url - request url + * @param data - request params (will be converted to query string) + * @param config - axios config + * @returns Promise + */ +export function GET( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.get(url, { + params: data, + ...config + }) +} + +/** + * POST request + * @param url - request url + * @param data - request body data + * @param config - axios config + * @returns Promise + */ +export function POST( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.post(url, data, config) +} + +/** + * DELETE request + * @param url - request url + * @param data - request params (will be converted to query string) + * @param config - axios config + * @returns Promise + */ +export function DELETE( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.delete(url, { + params: data, + ...config + }) +} + +/** + * PATCH request + * @param url - request url + * @param data - request body data + * @param config - axios config + * @returns Promise + */ +export function PATCH( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.patch(url, data, config) +} + +/** + * PUT request + * @param url - request url + * @param data - request body data + * @param config - axios config + * @returns Promise + */ +export function PUT( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.put(url, data, config) +} diff --git a/frontend/providers/aiproxy/utils/frontend/user.ts b/frontend/providers/aiproxy/utils/frontend/user.ts new file mode 100644 index 000000000000..d95f55d156ed --- /dev/null +++ b/frontend/providers/aiproxy/utils/frontend/user.ts @@ -0,0 +1,15 @@ +import { useSessionStore } from '@/store/session' + +export const getAppToken = () => { + let token = process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_MOCK_USER || '' : '' + + if (!token) { + // 从 store 获取 token + const { session } = useSessionStore.getState() + if (session?.token) { + token = session.token + } + } + + return token +} diff --git a/frontend/providers/aiproxy/utils/request.ts b/frontend/providers/aiproxy/utils/request.ts deleted file mode 100644 index f559b7e801bd..000000000000 --- a/frontend/providers/aiproxy/utils/request.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ApiResp } from '@/types/api' -import axios, { - InternalAxiosRequestConfig, - AxiosHeaders, - AxiosResponse, - AxiosRequestConfig -} from 'axios' -import { getUserSession } from './user' - -const request = axios.create({ - baseURL: '/', - withCredentials: true, - timeout: 60000 -}) - -// request interceptor -request.interceptors.request.use( - (config: InternalAxiosRequestConfig) => { - // auto append service prefix - if (config.url && !config.url?.startsWith('/api/')) { - config.url = '' + config.url - } - let _headers: AxiosHeaders = config.headers - - //获取token,并将其添加至请求头中 - _headers['Authorization'] = getUserSession() - - if (!config.headers || config.headers['Content-Type'] === '') { - _headers['Content-Type'] = 'application/json' - } - - config.headers = _headers - return config - }, - (error: any) => { - error.data = {} - error.data.msg = '服务器异常,请联系管理员!' - return Promise.resolve(error) - } -) - -// response interceptor -request.interceptors.response.use( - (response: AxiosResponse) => { - const { status, data } = response - if (status < 200 || status >= 300) { - return Promise.reject(status + ', ' + typeof data === 'string' ? data : String(data)) - } - - const apiResp = data as ApiResp - if (apiResp.code < 200 || apiResp.code >= 400) { - return Promise.reject(apiResp.code + ':' + apiResp.message) - } - - response.data = apiResp.data - return response.data - }, - (error: any) => { - if (axios.isCancel(error)) { - return Promise.reject('cancel request' + String(error)) - } else { - error.errMessage = '请求超时或服务器异常,请检查网络或联系管理员!' - } - return Promise.reject(error) - } -) - -export function GET( - url: string, - data?: { [key: string]: any }, - config?: AxiosRequestConfig -): Promise { - return request.get(url, { - params: data, - ...config - }) -} - -export function POST( - url: string, - data?: { [key: string]: any }, - config?: AxiosRequestConfig -): Promise { - return request.post(url, data, config) -} - -export function DELETE( - url: string, - data?: { [key: string]: any }, - config?: AxiosRequestConfig -): Promise { - return request.delete(url, { - params: data, - ...config - }) -} diff --git a/frontend/providers/aiproxy/utils/user.ts b/frontend/providers/aiproxy/utils/user.ts deleted file mode 100644 index c60c02883dcc..000000000000 --- a/frontend/providers/aiproxy/utils/user.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const getUserSession = () => { - let token: string = - process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_MOCK_USER || '' : '' - - try { - const store = localStorage.getItem('session') - if (!token && store) { - token = JSON.parse(store)?.token - } - } catch (err) { - err - } - return token -} diff --git a/frontend/providers/applaunchpad/data/config.yaml b/frontend/providers/applaunchpad/data/config.yaml index 998a3f0d05e7..c9174559f93d 100644 --- a/frontend/providers/applaunchpad/data/config.yaml +++ b/frontend/providers/applaunchpad/data/config.yaml @@ -19,6 +19,8 @@ launchpad: url: http://launchpad-monitor.sealos.svc.cluster.local:8428 billing: url: "http://account-service.account-system.svc:2333" + log: + url: "http://service-vlogs.sealos.svc.cluster.local:8428" appResourceFormSliderConfig: default: cpu: [100, 200, 500, 1000, 2000, 3000, 4000, 8000] diff --git a/frontend/providers/applaunchpad/package.json b/frontend/providers/applaunchpad/package.json index 761f2dcabe99..553a4f24d5a0 100644 --- a/frontend/providers/applaunchpad/package.json +++ b/frontend/providers/applaunchpad/package.json @@ -23,9 +23,11 @@ "@sealos/driver": "workspace:^", "@sealos/ui": "workspace:^", "@tanstack/react-query": "^4.35.3", + "@tanstack/react-table": "^8.10.7", "ansi_up": "^5.2.1", "axios": "^1.5.1", "base64-stream": "^1.0.0", + "date-fns": "^2.30.0", "dayjs": "^1.11.10", "decimal.js": "^10.4.3", "dns": "^0.2.2", @@ -45,6 +47,7 @@ "nprogress": "^0.2.0", "prettier": "^2.8.8", "react": "18.2.0", + "react-day-picker": "^8.8.2", "react-dom": "18.2.0", "react-hook-form": "^7.46.2", "react-i18next": "^14.1.2", diff --git a/frontend/providers/applaunchpad/public/locales/en/common.json b/frontend/providers/applaunchpad/public/locales/en/common.json index 0b7147775e8f..dc5a9fdd5113 100644 --- a/frontend/providers/applaunchpad/public/locales/en/common.json +++ b/frontend/providers/applaunchpad/public/locales/en/common.json @@ -51,8 +51,8 @@ "ConfigMap Path Conflict": "ConfigMap Path Conflict", "ConfigMap Tip": "ConfigMap", "Configurable number of instances or automatic horizontal scaling": "Configurable replica count and auto horizontal scaling", - "Configuration File": "Configmap", - "Confirm": "Yes", + "Configuration File": "Configmaps", + "Confirm": "Confirm", "Confirm deletion": "Yes", "Confirm Deploy Application?": "Are you sure you want to deploy the application?", "Confirm to restart this application?": "Are you sure you want to update the application?", @@ -86,7 +86,7 @@ "Edit Env Variable": "Edit Environment Variables", "Edit Environment Variables": "Edit Environment Variables", "Env Placeholder": "one per line, key and value separated by colon or equals sign, e.g.:\nmongoUrl=127.0.0.1:8000\nredisUrl:127.0.0.0:8001\n-env1 =test", - "Environment Variables": "Environment", + "Environment Variables": "Environment Variables", "Export": "Export", "Export Domain": "Assigned Domain", "file": "File", @@ -163,7 +163,7 @@ "please enter app name": "please enter: {{appName}}", "Pod": "Pod", "Pod Name": "Pod Name", - "Pods List": "Pods List", + "Pods List": "Pod List", "Port": "Port", "private": "private", "Private": "Private", @@ -197,7 +197,7 @@ "Stateless": "Stateless", "Status": "Status", "storage": "Storage", - "Storage": "Storage", + "Storage": "Mounted Volumes", "Storage path can not empty": "Storage mount path is required", "Storage Range": "Storage Range", "Storage Value can not empty": "Storage size is required", @@ -257,7 +257,7 @@ "total_price_tip": "The estimated cost does not include port fees and traffic fees, and is subject to actual usage.", "nodeports": "NodePorts", "streaming_logs": "Streaming logs", - "within_5_minutes": "Within 5 minutes", + "within_5_minute": "Within 5 minute", "within_1_hour": "Within 1 hour", "within_1_day": "Within 1 day", "terminated_logs": "Terminated logs", @@ -276,5 +276,46 @@ "add_configmap": "Add Configmaps", "storage_path_placeholder": "For Example: /data" }, - "guide_deploy_button": "Complete creation" -} \ No newline at end of file + "guide_deploy_button": "Complete creation", + "filter": "Filter", + "start": "Start", + "end": "End", + "time_zone": "Time Zone", + "recently": "Last", + "minute": "minutes", + "day": "days", + "hour": "hours", + "time": "Range", + "log_number": "Log Number", + "close": "Off", + "normal_filter": "Normal", + "advanced_filter": "Advance", + "json_mode": "JSON Mode", + "only_stderr": "Stderr Only", + "keyword": "Keywords", + "search": "Search", + "equal": "equal", + "greater_than": "Greater than", + "less_than": "Less than", + "field_name": "Select Field", + "value": "Enter Value", + "logNumber": "Log Counts", + "overview": "Overview", + "monitor": "Monitors", + "logs": "Logs", + "application_source": "Source", + "contains": "contains", + "not_contains": "not contains", + "visible": "Visable", + "hidden": "Hidden", + "export_log": "Export", + "no_data_available": "No data available", + "all": "All", + "hour-singular": "hour", + "not_equal": "not", + "field_settings": "Field Setting", + "selected": "Selected", + "please_select": "Please Select", + "refetching_success": "Refresh Successful", + "refresh": "refresh" +} diff --git a/frontend/providers/applaunchpad/public/locales/zh/common.json b/frontend/providers/applaunchpad/public/locales/zh/common.json index da5b88f3929c..107bf9a5b46f 100644 --- a/frontend/providers/applaunchpad/public/locales/zh/common.json +++ b/frontend/providers/applaunchpad/public/locales/zh/common.json @@ -30,7 +30,7 @@ "Auto scaling": "弹性伸缩", "Balance": "余额", "Basic Config": "基础配置", - "Basic Information": "基本信息", + "Basic Information": "基础信息", "Can help you deploy any Docker image": "丰富的镜像仓库,支持任意 Docker 镜像", "Can not change storage path": "不允许修改挂载路径", "Cancel": "取消", @@ -162,8 +162,8 @@ "Please enter": "请输入", "please enter app name": "请输入:{{appName}}", "Pod": "实例", - "Pod Name": "实例名", - "Pods List": "实例列表", + "Pod Name": "Pod 名称", + "Pods List": "Pod 列表", "Port": "端口", "private": "私有", "Private": "私有", @@ -257,7 +257,7 @@ "total_price_tip": "预估费用不包括端口费用和流量费用,以实际使用为准", "nodeports": "外网端口", "streaming_logs": "实时日志", - "within_5_minutes": "五分钟内", + "within_5_minute": "五分钟内", "within_1_hour": "一小时内", "within_1_day": "一天内", "terminated_logs": "中断前", @@ -277,5 +277,45 @@ "add_configmap": "新增配置文件", "storage_path_placeholder": "如:/data" }, - "guide_deploy_button": "完成创建" -} \ No newline at end of file + "guide_deploy_button": "完成创建", + "filter": "筛选", + "start": "开始", + "end": "结束", + "time_zone": "时区", + "recently": "最近", + "minute": "分钟", + "hour": "小时", + "day": "天", + "time": "时间", + "log_number": "日志数", + "close": "关闭", + "normal_filter": "普通筛选", + "advanced_filter": "高级筛选", + "json_mode": "JSON模式", + "only_stderr": "只看 Stderr", + "keyword": "关键词", + "search": "查询", + "field_name": "字段名", + "equal": "等于", + "value": "值", + "logNumber": "日志数量", + "overview": "概览", + "monitor": "监控", + "logs": "日志", + "application_source": "来源", + "contains": "包含", + "not_contains": "不包含", + "visible": "可见", + "hidden": "隐藏", + "piece": "个", + "export_log": "导出日志", + "no_data_available": "暂无数据", + "all": "全部", + "hour-singular": "小时", + "not_equal": "不等于", + "field_settings": "字段设置", + "selected": "已选中", + "please_select": "请选择", + "refetching_success": "刷新成功", + "refresh": "刷新" +} diff --git a/frontend/providers/applaunchpad/src/api/app.ts b/frontend/providers/applaunchpad/src/api/app.ts index b18c98e2d7cd..102464981652 100644 --- a/frontend/providers/applaunchpad/src/api/app.ts +++ b/frontend/providers/applaunchpad/src/api/app.ts @@ -9,6 +9,8 @@ import { } from '@/utils/adapt'; import type { AppPatchPropsType, PodDetailType } from '@/types/app'; import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor'; +import { LogQueryPayload } from '@/pages/api/log/queryLogs'; +import { PodListQueryPayload } from '@/pages/api/log/queryPodList'; export const postDeployApp = (yamlList: string[]) => POST('/api/applyApp', { yamlList }); @@ -58,4 +60,11 @@ export const getAppMonitorData = (payload: { queryName: string; queryKey: keyof MonitorQueryKey; step: string; + start?: number; + end?: number; }) => GET(`/api/monitor/getMonitorData`, payload); + +export const getAppLogs = (payload: LogQueryPayload) => POST('/api/log/queryLogs', payload); + +export const getLogPodList = (payload: PodListQueryPayload) => + POST('/api/log/queryPodList', payload); diff --git a/frontend/providers/applaunchpad/src/components/AdvancedSelect/index.tsx b/frontend/providers/applaunchpad/src/components/AdvancedSelect/index.tsx new file mode 100644 index 000000000000..e90f931ad73b --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/AdvancedSelect/index.tsx @@ -0,0 +1,267 @@ +'use client'; + +import { + Menu, + Box, + MenuList, + MenuItem, + Button, + useDisclosure, + useOutsideClick, + MenuButton, + Flex, + Checkbox +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import React, { useRef, forwardRef, useMemo } from 'react'; +import type { BoxProps, ButtonProps } from '@chakra-ui/react'; + +export interface ListItem { + label: string | React.ReactNode; + value: string; + checked: boolean; +} + +interface Props extends ButtonProps { + width?: string; + height?: string; + value?: string; + placeholder?: string; + list: ListItem[]; + onchange?: (val: string) => void; + onCheckboxChange?: (list: ListItem[]) => void; + isInvalid?: boolean; + boxStyle?: BoxProps; + checkBoxMode?: boolean; +} + +const AdvancedSelect = ( + { + placeholder, + leftIcon, + value, + width = 'auto', + height = '30px', + list, + onchange, + onCheckboxChange, + isInvalid, + boxStyle, + checkBoxMode = false, + ...props + }: Props, + selectRef: any +) => { + const { t } = useTranslation(); + + const ref = useRef(null); + const SelectRef = useRef(null); + const { isOpen, onOpen, onClose } = useDisclosure(); + + useOutsideClick({ + ref: SelectRef, + handler: () => { + onClose(); + } + }); + + const displayText = useMemo(() => { + const selectedCount = checkBoxMode ? list.filter((item) => item.checked).length : 0; + const activeMenu = list.find((item) => item.value === value); + + if (!checkBoxMode) { + return activeMenu ? activeMenu.label : placeholder; + } + if (selectedCount === 0) { + return placeholder; + } + if (selectedCount === list.length) { + return t('all'); + } + return `${t('selected')} ${selectedCount}`; + }, [checkBoxMode, list, t, value, placeholder]); + + return ( + + + } + width={width} + height={height} + ref={ref} + display={'flex'} + alignItems={'center'} + justifyContent={'center'} + border={'1px solid #E8EBF0'} + borderRadius={'md'} + fontSize={'12px'} + fontWeight={'400'} + color={'grayModern.900'} + variant={'outline'} + _hover={{ + borderColor: 'brightBlue.300', + bg: 'grayModern.50' + }} + _active={{ + transform: '' + }} + boxShadow={'none'} + {...(isOpen + ? { + // boxShadow: '0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)', + borderColor: 'brightBlue.500', + bg: '#FFF' + } + : { + bg: '#F7F8FA', + borderColor: isInvalid ? 'red' : '' + })} + onClick={() => { + isOpen ? onClose() : onOpen(); + }} + {...props} + > + + {displayText} + + + + { + const w = ref.current?.clientWidth; + if (w) { + return `${w}px !important`; + } + return Array.isArray(width) + ? width.map((item) => `${item} !important`) + : `${width} !important`; + })()} + p={'6px'} + borderRadius={'base'} + border={'1px solid #E8EBF0'} + boxShadow={ + '0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)' + } + zIndex={99} + overflow={'overlay'} + maxH={'300px'} + > + {checkBoxMode && ( + + item.checked)} + onChange={() => { + if (onCheckboxChange) { + const newList = list.map((item) => ({ + ...item, + checked: !list.every((item) => item.checked) + })); + onCheckboxChange(newList); + } + }} + sx={{ + 'span.chakra-checkbox__control[data-checked]': { + background: '#f0f4ff', + border: '1px solid #219bf4 ', + boxShadow: '0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)', + color: '#219bf4', + borderRadius: '4px' + }, + 'span.chakra-checkbox__control': { + background: 'white', + border: '1px solid #E8EBF0', + borderRadius: '4px' + } + }} + > + {t('all')} + + + )} + + {list.map((item, index) => ( + { + if (onchange && value !== item.value) { + onchange(item.value); + } + }} + > + {checkBoxMode ? ( + { + if (onCheckboxChange) { + const newList = list.map((listItem) => + listItem.value === item.value + ? { ...listItem, checked: !listItem.checked } + : listItem + ); + onCheckboxChange(newList); + } + }} + sx={{ + 'span.chakra-checkbox__control[data-checked]': { + background: '#f0f4ff ', + border: '1px solid #219bf4 ', + boxShadow: '0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)', + color: '#219bf4', + borderRadius: '4px' + }, + 'span.chakra-checkbox__control': { + background: 'white', + border: '1px solid #E8EBF0', + borderRadius: '4px' + } + }} + > + {item.label} + + ) : ( + {item.label} + )} + + ))} + + + + ); +}; + +export default forwardRef(AdvancedSelect); diff --git a/frontend/providers/applaunchpad/src/components/BaseTable/SwitchPage.tsx b/frontend/providers/applaunchpad/src/components/BaseTable/SwitchPage.tsx new file mode 100644 index 000000000000..fe1c44dd4e20 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/BaseTable/SwitchPage.tsx @@ -0,0 +1,158 @@ +import { Button, ButtonProps, Flex, FlexProps, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +import { Icon, IconProps } from '@chakra-ui/react'; + +export function ToLeftIcon(props: IconProps) { + return ( + + + + ); +} + +export function RightFirstIcon(props: IconProps) { + return ( + + + + ); +} + +export function SwitchPage({ + totalPage, + totalItem, + pageSize, + currentPage, + setCurrentPage, + isPreviousData, + ...props +}: { + currentPage: number; + totalPage: number; + totalItem: number; + pageSize: number; + isPreviousData?: boolean; + setCurrentPage: (idx: number) => void; +} & FlexProps) { + const { t } = useTranslation(); + const switchStyle: ButtonProps = { + width: '24px', + height: '24px', + minW: '0', + background: 'grayModern.250', + flexGrow: '0', + borderRadius: 'full', + // variant:'unstyled', + _hover: { + background: 'grayModern.150', + minW: '0' + }, + _disabled: { + borderRadius: 'full', + background: 'grayModern.150', + cursor: 'not-allowed', + minW: '0' + } + }; + return ( + + + {t('Total')}: + + + {totalItem} + + + + + {currentPage} + / + {totalPage} + + + + + {pageSize} + + + /{t('Page')} + + + ); +} diff --git a/frontend/providers/applaunchpad/src/components/BaseTable/index.tsx b/frontend/providers/applaunchpad/src/components/BaseTable/index.tsx new file mode 100644 index 000000000000..fc867a16879b --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/BaseTable/index.tsx @@ -0,0 +1,120 @@ +import { + HTMLChakraProps, + Spinner, + Table, + TableContainer, + TableContainerProps, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { Column, Table as ReactTable, flexRender } from '@tanstack/react-table'; +import { CSSProperties } from 'react'; + +const getCommonPinningStyles = (column: Column): CSSProperties => { + const isPinned = column.getIsPinned(); + + return { + position: isPinned ? 'sticky' : 'relative', + left: isPinned === 'left' ? 0 : undefined, + right: isPinned === 'right' ? 0 : undefined, + zIndex: isPinned ? 10 : 0 + }; +}; + +export function BaseTable({ + table, + isLoading, + tdStyle, + isHeaderFixed = false, + ...props +}: { + table: ReactTable; + isLoading: boolean; + tdStyle?: HTMLChakraProps<'td'>; + isHeaderFixed?: boolean; +} & TableContainerProps) { + return ( + + + + {table.getHeaderGroups().map((headers) => { + return ( + + {headers.headers.map((header, i) => { + return ( + + ); + })} + + ); + })} + + + {isLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((item, index) => { + return ( + + {item.getAllCells().map((cell, i) => { + const isPinned = cell.column.getIsPinned(); + return ( + + ); + })} + + ); + }) + )} + +
)} + > + {flexRender(header.column.columnDef.header, header.getContext())} +
+ +
)} + {...tdStyle} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ); +} diff --git a/frontend/providers/applaunchpad/src/components/DatePicker/index.tsx b/frontend/providers/applaunchpad/src/components/DatePicker/index.tsx new file mode 100644 index 000000000000..746c60dcc6c9 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/DatePicker/index.tsx @@ -0,0 +1,485 @@ +'use client'; + +import { + Button, + ButtonGroup, + Divider, + Flex, + FlexProps, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Text, + useDisclosure +} from '@chakra-ui/react'; +import { endOfDay, format, isAfter, isBefore, isMatch, isValid, parse, startOfDay } from 'date-fns'; +import { enUS, zhCN } from 'date-fns/locale'; +import { useTranslation } from 'next-i18next'; +import { ChangeEventHandler, useMemo, useState } from 'react'; +import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker'; +import useDateTimeStore from '@/store/date'; +import { formatTimeRange, parseTimeRange } from '@/utils/timeRange'; +import { MySelect } from '@sealos/ui'; +import MyIcon from '../Icon'; + +interface DatePickerProps extends FlexProps { + isDisabled?: boolean; +} + +interface RecentDate { + label: string; + value: DateRange; + compareValue: string; +} + +const DatePicker = ({ isDisabled = false, ...props }: DatePickerProps) => { + const { t, i18n } = useTranslation(); + const currentLang = i18n.language; + const { isOpen, onClose, onOpen } = useDisclosure(); + + const { startDateTime, endDateTime, setStartDateTime, setEndDateTime, timeZone, setTimeZone } = + useDateTimeStore(); + + const initState = { + from: startDateTime, + to: endDateTime + }; + + const recentDateList = useMemo( + () => [ + { + label: `${t('recently')} 5 ${t('minute')}`, + value: getDateRange('5m'), + compareValue: '5m' + }, + { + label: `${t('recently')} 15 ${t('minute')}`, + value: getDateRange('15m'), + compareValue: '15m' + }, + { + label: `${t('recently')} 30 ${t('minute')}`, + value: getDateRange('30m'), + compareValue: '30m' + }, + { + label: `${t('recently')} 1 ${t('hour-singular')}`, + value: getDateRange('1h'), + compareValue: '1h' + }, + { + label: `${t('recently')} 3 ${t('hour')}`, + value: getDateRange('3h'), + compareValue: '3h' + }, + { + label: `${t('recently')} 6 ${t('hour')}`, + value: getDateRange('6h'), + compareValue: '6h' + }, + { + label: `${t('recently')} 24 ${t('hour')}`, + value: getDateRange('24h'), + compareValue: '24h' + }, + { + label: `${t('recently')} 2 ${t('day')}`, + value: getDateRange('2d'), + compareValue: '2d' + }, + { + label: `${t('recently')} 3 ${t('day')}`, + value: getDateRange('3d'), + compareValue: '3d' + }, + { + label: `${t('recently')} 7 ${t('day')}`, + value: getDateRange('7d'), + compareValue: '7d' + } + ], + [t] + ); + + const defaultRecentDate = useMemo(() => { + const currentTimeRange = formatTimeRange(startDateTime, endDateTime); + return ( + recentDateList.find((item) => item.compareValue === currentTimeRange) || + recentDateList.find((item) => item.compareValue === '30m') || + recentDateList[0] + ); + }, [startDateTime, endDateTime, recentDateList]); + + const [inputState, setInputState] = useState<0 | 1>(0); + const [recentDate, setRecentDate] = useState(defaultRecentDate); + + const [fromDateString, setFromDateString] = useState(format(initState.from, 'y-MM-dd')); + const [toDateString, setToDateString] = useState(format(initState.to, 'y-MM-dd')); + const [fromTimeString, setFromTimeString] = useState(format(initState.from, 'HH:mm:ss')); + const [toTimeString, setToTimeString] = useState(format(initState.to, 'HH:mm:ss')); + + const [fromDateError, setFromDateError] = useState(null); + const [toDateError, setToDateError] = useState(null); + const [fromTimeError, setFromTimeError] = useState(null); + const [toTimeError, setToTimeError] = useState(null); + const [fromDateShake, setFromDateShake] = useState(false); + const [toDateShake, setToDateShake] = useState(false); + const [fromTimeShake, setFromTimeShake] = useState(false); + const [toTimeShake, setToTimeShake] = useState(false); + + const [selectedRange, setSelectedRange] = useState(initState); + + const onSubmit = () => { + if (fromDateError || fromTimeError || toDateError || toTimeError) { + if (fromDateError) setFromDateShake(true); + if (toDateError) setToDateShake(true); + if (fromTimeError) setFromTimeShake(true); + if (toTimeError) setToTimeShake(true); + setTimeout(() => { + setFromDateShake(false); + setToDateShake(false); + setFromTimeShake(false); + setToTimeShake(false); + }, 300); + + return; + } + selectedRange?.from && setStartDateTime(selectedRange.from); + selectedRange?.to && setEndDateTime(selectedRange.to); + onClose(); + }; + + const handleFromChange = (value: string, type: 'date' | 'time') => { + let newDateTimeString; + + if (type === 'date') { + setFromDateString(value); + if (!isMatch(value, 'y-MM-dd')) { + setFromDateError('Invalid date format'); + return; + } + setFromDateError(null); + newDateTimeString = `${value} ${fromTimeString}`; + } else { + setFromTimeString(value); + if (!isMatch(value, 'HH:mm:ss')) { + setFromTimeError('Invalid time format'); + return; + } + setFromTimeError(null); + newDateTimeString = `${fromDateString} ${value}`; + } + + console.log(newDateTimeString); + + const date = parse(newDateTimeString, 'y-MM-dd HH:mm:ss', new Date()); + + if (!isValid(date)) { + return setSelectedRange({ from: undefined, to: selectedRange?.to }); + } + + if (selectedRange?.to) { + if (isAfter(date, selectedRange.to)) { + setSelectedRange({ from: selectedRange.to, to: date }); + } else { + setSelectedRange({ from: date, to: selectedRange?.to }); + } + } else { + setSelectedRange({ from: date, to: date }); + } + }; + + const handleToChange = (value: string, type: 'date' | 'time') => { + let newDateTimeString; + + if (type === 'date') { + setToDateString(value); + if (!isMatch(value, 'y-MM-dd')) { + setToDateError('Invalid date format'); + return; + } + setToDateError(null); + newDateTimeString = `${value} ${toTimeString}`; + } else { + setToTimeString(value); + if (!isMatch(value, 'HH:mm:ss')) { + setToTimeError('Invalid time format'); + return; + } + setToTimeError(null); + newDateTimeString = `${toDateString} ${value}`; + } + + const date = parse(newDateTimeString, 'y-MM-dd HH:mm:ss', new Date()); + + if (!isValid(date)) { + return setSelectedRange({ from: selectedRange?.from, to: undefined }); + } + if (selectedRange?.from) { + if (isBefore(date, selectedRange.from)) { + setSelectedRange({ from: date, to: selectedRange.from }); + } else { + setSelectedRange({ from: selectedRange?.from, to: date }); + } + } else { + setSelectedRange({ from: date, to: date }); + } + }; + + const handleRangeSelect: SelectRangeEventHandler = (range: DateRange | undefined) => { + if (range) { + let { from, to } = range; + if (inputState === 0) { + // from + if (from === selectedRange?.from) { + // when 'to' is changed + from = to; + } else { + to = from; + } + setInputState(1); + } else { + setInputState(0); + } + setSelectedRange({ + from, + to + }); + if (from) { + setFromDateString(format(startOfDay(from), 'y-MM-dd')); + setFromTimeString(format(startOfDay(from), 'HH:mm:ss')); + } else { + setFromDateString(format(new Date(), 'y-MM-dd')); + setFromTimeString(format(new Date(), 'HH:mm:ss')); + } + if (to) { + setToDateString(format(endOfDay(to), 'y-MM-dd')); + setToTimeString(format(endOfDay(to), 'HH:mm:ss')); + } else { + setToDateString(format(from ? from : new Date(), 'y-MM-dd')); + setToTimeString(format(from ? from : new Date(), 'HH:mm:ss')); + } + } else { + // default is cancel + if (fromDateString && fromTimeString && selectedRange?.from) { + setToDateString(fromDateString); + setToTimeString(fromTimeString); + setSelectedRange({ + ...selectedRange, + to: selectedRange.from + }); + setInputState(1); + } + } + }; + + const handleRecentDateClick = (item: RecentDate) => { + setFromDateError(null); + setFromTimeError(null); + setToDateError(null); + setToTimeError(null); + + setRecentDate(item); + setSelectedRange(item.value); + if (item.value.from) { + setFromDateString(format(item.value.from, 'y-MM-dd')); + setFromTimeString(format(item.value.from, 'HH:mm:ss')); + } + if (item.value.to) { + setToDateString(format(item.value.to, 'y-MM-dd')); + setToTimeString(format(item.value.to, 'HH:mm:ss')); + } + }; + + return ( + + + + + {format(startDateTime, 'y-MM-dd HH:mm:ss')} + + {format(endDateTime, 'y-MM-dd HH:mm:ss')} + + + + + + + + + + + {t('start')} + + + handleFromChange(e.target.value, 'date')} + error={!!fromDateError} + showError={fromDateShake} + /> + handleFromChange(e.target.value, 'time')} + error={!!fromTimeError} + showError={fromTimeShake} + /> + + + + + + {t('end')} + + + handleToChange(e.target.value, 'date')} + error={!!toDateError} + showError={toDateShake} + /> + handleToChange(e.target.value, 'time')} + error={!!toTimeError} + showError={toTimeShake} + /> + + + + + + + {recentDateList.map((item) => ( + + ))} + + + + + + setTimeZone(val)} + /> + + + + + + + + + + ); +}; + +interface DatePickerInputProps { + value: string; + onChange: ChangeEventHandler | undefined; + error: boolean; + showError: boolean; +} + +const DatePickerInput = ({ value, onChange, error, showError }: DatePickerInputProps) => { + return ( + + ); +}; + +const getDateRange = (value: string): DateRange => { + const { startTime: from, endTime: to } = parseTimeRange(value); + return { from, to }; +}; + +export default DatePicker; diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/arrowLeft.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/arrowLeft.svg index 9e246b287599..045c63cbce5f 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/icons/arrowLeft.svg +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/arrowLeft.svg @@ -1,10 +1,3 @@ - - - - - - - - - + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/arrowRight.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/arrowRight.svg new file mode 100644 index 000000000000..80e943436d2d --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/arrowRight.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/calendar.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/calendar.svg new file mode 100644 index 000000000000..f864a731f268 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/chart.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/chart.svg new file mode 100644 index 000000000000..c6902b1966cf --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/chart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/configMap.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/configMap.svg index f027353e49b0..88d5f534019e 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/icons/configMap.svg +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/configMap.svg @@ -1 +1,11 @@ - \ No newline at end of file + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/container.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/container.svg new file mode 100644 index 000000000000..c15441ffd1cf --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/container.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/copy.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/copy.svg index bfd38df8edea..adc04ee6850f 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/icons/copy.svg +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/copy.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/emptyChart.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/emptyChart.svg new file mode 100644 index 000000000000..af039d00acee --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/emptyChart.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/export.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/export.svg new file mode 100644 index 000000000000..6fbd16dbd6a4 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/export.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg index 798a41665ebb..5257fb78e837 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/monitor.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/monitor.svg new file mode 100644 index 000000000000..c81668789e4b --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/monitor.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/refresh.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/refresh.svg new file mode 100644 index 000000000000..302e566a1f57 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/refresh.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/store.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/store.svg index 7e43075a539f..d212d3c1169b 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/icons/store.svg +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/store.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/to.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/to.svg new file mode 100644 index 000000000000..af8c21bdf833 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/to.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/providers/applaunchpad/src/components/Icon/index.tsx b/frontend/providers/applaunchpad/src/components/Icon/index.tsx index bf1f410e152f..b4e2ceb9fe05 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/index.tsx +++ b/frontend/providers/applaunchpad/src/components/Icon/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { IconProps } from '@chakra-ui/react'; import { Icon } from '@chakra-ui/react'; -const map = { +export const IconMap = { more: require('./icons/more.svg').default, store: require('./icons/store.svg').default, configMap: require('./icons/configMap.svg').default, @@ -51,20 +51,35 @@ const map = { upload: require('./icons/upload.svg').default, search: require('./icons/search.svg').default, pods: require('./icons/pods.svg').default, + monitor: require('./icons/monitor.svg').default, hardDrive: require('./icons/hardDrive.svg').default, - download: require('./icons/download.svg').default + download: require('./icons/download.svg').default, + calendar: require('./icons/calendar.svg').default, + to: require('./icons/to.svg').default, + refresh: require('./icons/refresh.svg').default, + container: require('./icons/container.svg').default, + arrowRight: require('./icons/arrowRight.svg').default, + chart: require('./icons/chart.svg').default, + export: require('./icons/export.svg').default }; -export type IconType = keyof typeof map; +export type IconType = keyof typeof IconMap; const MyIcon = ({ name, w = 'auto', h = 'auto', ...props -}: { name: keyof typeof map } & IconProps) => { - return map[name] ? ( - +}: { name: keyof typeof IconMap } & IconProps) => { + return IconMap[name] ? ( + ) : null; }; diff --git a/frontend/providers/applaunchpad/src/components/LangSelect/index.tsx b/frontend/providers/applaunchpad/src/components/LangSelect/index.tsx index d848a87309f5..3e1401a1d1a8 100644 --- a/frontend/providers/applaunchpad/src/components/LangSelect/index.tsx +++ b/frontend/providers/applaunchpad/src/components/LangSelect/index.tsx @@ -1,6 +1,6 @@ import { setLangStore } from '@/utils/cookieUtils'; import { Menu, MenuButton, MenuButtonProps, MenuItem, MenuList, Text } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; const langIcon = ( { + const { screenWidth } = useGlobalStore(); + const xData = useMemo( + () => + data?.xData?.map((time) => dayjs(time * 1000).format('MM-DD HH:mm')) || new Array(30).fill(0), + [data?.xData] + ); + const yData = data?.yData || new Array(30).fill(''); + + const Dom = useRef(null); + const myChart = useRef(); + const resizeObserver = useRef(); + + const optionStyle = useMemo( + () => ({ + areaStyle: { + color: map[type].backgroundColor + }, + lineStyle: { + width: '1', + color: map[type].lineColor + }, + itemStyle: { + width: 1.5, + color: map[type].lineColor + } + }), + [type] + ); + + const option = useRef({ + xAxis: { + type: 'category', + data: xData, + boundaryGap: true, + axisLine: { + lineStyle: { + color: '#E8EBF0' + } + }, + axisLabel: { + color: '#667085' + } + }, + yAxis: { + type: 'value', + splitNumber: 3, + splitLine: { + lineStyle: { + type: 'dashed', + color: '#E4E7EC' + } + } + }, + series: [ + { + data: yData, + type: 'bar', + animationDuration: 300, + barWidth: '90%', + ...optionStyle + } + ], + grid: { + left: 0, + right: 0, + bottom: 0, + top: 5, + containLabel: true + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'line' + }, + formatter: (params: any[]) => { + const axisValue = params[0]?.axisValue; + return `${axisValue} ${params[0]?.value || 0}`; + } + } + }); + + // init chart + useEffect(() => { + if (!Dom.current || myChart?.current?.getOption() || !visible) return; + myChart.current = echarts.init(Dom.current); + myChart.current && myChart.current.setOption(option.current); + }, [Dom, visible]); + + // data changed, update + useEffect(() => { + if (!myChart.current || !myChart?.current?.getOption() || !visible) return; + option.current.xAxis.data = xData; + option.current.series[0].data = yData; + myChart.current.setOption(option.current); + }, [xData, yData, visible]); + + // type changed, update + useEffect(() => { + if (!myChart.current || !myChart?.current?.getOption()) return; + option.current.series[0] = { + ...option.current.series[0], + ...optionStyle + }; + myChart.current.setOption(option.current); + }, [optionStyle]); + + // resize chart + useEffect(() => { + if (!myChart.current || !myChart.current.getOption()) return; + myChart.current.resize(); + }, [screenWidth]); + + useEffect(() => { + if (!Dom.current || !visible) return; + + resizeObserver.current = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry?.contentRect && myChart.current) { + if (entry.contentRect.width > 0 && entry.contentRect.height > 0) { + myChart.current.resize(); + } + } + }); + + resizeObserver.current.observe(Dom.current); + + return () => { + resizeObserver.current?.disconnect(); + }; + }, [visible]); + + useEffect(() => { + return () => { + if (myChart.current) { + myChart.current.dispose(); + } + resizeObserver.current?.disconnect(); + }; + }, []); + + return
; +}; + +export default LogBarChart; diff --git a/frontend/providers/applaunchpad/src/components/Monitor/Header.tsx b/frontend/providers/applaunchpad/src/components/Monitor/Header.tsx new file mode 100644 index 000000000000..97ae5ea38cf3 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Monitor/Header.tsx @@ -0,0 +1,158 @@ +import MyIcon from '@/components/Icon'; +import { + Box, + Button, + ButtonGroup, + Flex, + Menu, + MenuButton, + MenuItem, + MenuList, + Text +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import AdvancedSelect, { ListItem } from '../AdvancedSelect'; +import DynamicTime from './Time'; + +import { REFRESH_INTERVAL_OPTIONS } from '@/constants/monitor'; +import useDateTimeStore from '@/store/date'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import dynamic from 'next/dynamic'; +import { MyTooltip } from '@sealos/ui'; +const DatePicker = dynamic(() => import('@/components/DatePicker'), { ssr: false }); + +export default function Header({ + podList, + setPodList, + refetchData +}: { + podList: ListItem[]; + setPodList: (val: ListItem[]) => void; + refetchData: () => void; +}) { + const { t } = useTranslation(); + const { refreshInterval, setRefreshInterval } = useDateTimeStore(); + + return ( + + + + {t('monitor')} + + + ({t('Update Time')}   + ) + + + + } + width={'fit-content'} + value={'hello-sql-postgresql-0'} + list={podList} + onCheckboxChange={(val) => { + setPodList(val); + }} + placeholder={t('please_select')} + /> + + + + + + + + + + {refreshInterval === 0 ? null : ( + {`${refreshInterval / 1000}s`} + )} + + + + + {REFRESH_INTERVAL_OPTIONS.map((item) => ( + { + setRefreshInterval(item.value); + }} + {...(refreshInterval === item.value + ? { + color: 'brightBlue.600' + } + : {})} + borderRadius={'4px'} + _hover={{ + bg: 'rgba(17, 24, 36, 0.05)', + color: 'brightBlue.600' + }} + p={'6px'} + > + {item.label} + + ))} + + + + + ); +} diff --git a/frontend/providers/applaunchpad/src/components/Monitor/Time.tsx b/frontend/providers/applaunchpad/src/components/Monitor/Time.tsx new file mode 100644 index 000000000000..f9fee040c006 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Monitor/Time.tsx @@ -0,0 +1,18 @@ +import { useState, useEffect } from 'react'; +import dayjs from 'dayjs'; +import { Box } from '@chakra-ui/react'; + +export default function DynamicTime() { + const [time, setTime] = useState('--:--:--'); + + useEffect(() => { + setTime(dayjs().format('HH:mm:ss')); + const timer = setInterval(() => { + setTime(dayjs().format('HH:mm:ss')); + }, 1000); + + return () => clearInterval(timer); + }, []); + + return {time}; +} diff --git a/frontend/providers/applaunchpad/src/components/MonitorChart/index.module.css b/frontend/providers/applaunchpad/src/components/MonitorChart/index.module.css new file mode 100644 index 000000000000..24a191906a60 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/MonitorChart/index.module.css @@ -0,0 +1,58 @@ +.tooltip { + background: white; + border-radius: 8px; + padding: 16px; + border: 1px solid #e8ebf0; + box-shadow: 0px 24px 48px -12px rgba(19, 51, 107, 0.2), 0px 0px 1px 0px rgba(19, 51, 107, 0.2); +} + +.tooltipHeader { + font-size: 12px; + font-weight: 500; + color: #111824; + margin-bottom: 12px; + border-bottom: 1px solid #eee; + padding-bottom: 12px; +} + +.tooltipItem { + display: flex; + align-items: center; +} + +.tooltipItem:not(:last-child) { + margin-bottom: 12px; +} + +.tooltipDot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 8px; +} + +.tooltipName { + color: #333; + margin-right: 12px; +} + +.tooltipValue { + font-weight: 500; + margin-right: 12px; +} + +.tooltipButton { + display: flex; + align-items: center; + gap: 4px; + margin-left: auto; + background: #f4f4f7; + color: #485264; + border: none; + padding: 6px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; +} diff --git a/frontend/providers/applaunchpad/src/components/MonitorChart/index.tsx b/frontend/providers/applaunchpad/src/components/MonitorChart/index.tsx new file mode 100644 index 000000000000..da851edfba4b --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/MonitorChart/index.tsx @@ -0,0 +1,250 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +import * as echarts from 'echarts'; +import { useGlobalStore } from '@/store/global'; +import dayjs from 'dayjs'; +import { LineStyleMap } from '@/constants/monitor'; +import { Flex, FlexProps, Text } from '@chakra-ui/react'; +import MyIcon from '../Icon'; +import { useTranslation } from 'next-i18next'; +import styles from './index.module.css'; + +type MonitorChart = FlexProps & { + data: { + xData: string[]; + yData: { + name: string; + type: string; + data: number[]; + lineStyleType?: string; + }[]; + }; + type?: 'blue' | 'deepBlue' | 'green' | 'purple'; + title: string; + yAxisLabelFormatter?: (value: number) => string; + yDataFormatter?: (values: number[]) => number[]; + unit?: string; + isShowLegend?: boolean; +}; + +const MonitorChart = ({ + type, + data, + title, + yAxisLabelFormatter, + yDataFormatter, + unit, + isShowLegend = true, + ...props +}: MonitorChart) => { + const { screenWidth } = useGlobalStore(); + const chartDom = useRef(null); + const myChart = useRef(); + const { t } = useTranslation(); + + const option = useMemo( + () => ({ + tooltip: { + trigger: 'axis', + enterable: true, + extraCssText: ` + box-shadow: none; + padding: 0; + background-color: transparent; + border: none; + `, + formatter: (params: any) => { + let axisValue = params[0]?.axisValue; + return ` +
+
${axisValue}
+ ${params + .map( + (item: any) => ` +
+ + ${item.seriesName} + ${item.value}${unit || ''} + +
+ ` + ) + .join('')} +
+ `; + }, + + // @ts-ignore + position: (point, params, dom, rect, size) => { + let xPos = point[0]; + let yPos = point[1] + 10; + let chartWidth = size.viewSize[0]; + let chartHeight = size.viewSize[1]; + let tooltipWidth = dom.offsetWidth; + let tooltipHeight = dom.offsetHeight; + + if (xPos + tooltipWidth > chartWidth) { + xPos = xPos - tooltipWidth; + } + + if (xPos < 0) { + xPos = 0; + } + + return [xPos, yPos]; + } + }, + grid: { + left: '4px', + bottom: '4px', + top: '10px', + right: '20px', + containLabel: true + }, + xAxis: { + show: true, + type: 'category', + offset: 4, + boundaryGap: false, + axisLabel: { + interval: (index: number, value: string) => { + const total = data?.xData?.length || 0; + if (index === 0 || index === total - 1) return false; + return index % Math.floor(total / 6) === 0; + }, + textStyle: { + color: '#667085' + }, + hideOverlap: true + }, + axisTick: { + show: false + }, + axisLine: { + show: true, + lineStyle: { + color: '#E4E7EC', + type: 'solid' + } + }, + data: data?.xData?.map((time) => dayjs(parseFloat(time) * 1000).format('MM-DD HH:mm')) + }, + yAxis: { + type: 'value', + splitNumber: 2, + max: 100, + min: 0, + boundaryGap: false, + axisLabel: { + formatter: yAxisLabelFormatter + }, + axisLine: { + show: false + }, + splitLine: { + lineStyle: { + type: 'dashed', + color: '#E4E7EC' + } + } + }, + series: data?.yData?.map((item, index) => { + return { + name: item.name, + data: item.data, + type: 'line', + smooth: true, + showSymbol: false, + animationDuration: 300, + animationEasingUpdate: 'linear', + areaStyle: { + color: LineStyleMap[index % LineStyleMap.length].backgroundColor + }, + lineStyle: { + width: '1', + color: LineStyleMap[index % LineStyleMap.length].lineColor, + type: item?.lineStyleType || 'solid' + }, + itemStyle: { + width: 1.5, + color: LineStyleMap[index % LineStyleMap.length].lineColor + }, + emphasis: { + // highlight + disabled: true + } + }; + }) + }), + [data?.xData, data?.yData] + ); + + useEffect(() => { + if (!chartDom.current) return; + + if (!myChart.current) { + myChart.current = echarts.init(chartDom.current); + } else { + myChart.current.dispose(); + myChart.current = echarts.init(chartDom.current); + } + + myChart.current.setOption(option); + }, [data, option]); + + useEffect(() => { + return () => { + if (myChart.current) { + myChart.current.dispose(); + } + }; + }, []); + + // resize chart + useEffect(() => { + if (!myChart.current || !myChart.current.getOption()) return; + myChart.current.resize(); + }, [screenWidth]); + + return ( + + + {isShowLegend && ( + + {data?.yData?.map((item, index) => ( + + + + {item?.name} + + + ))} + + )} + + ); +}; + +export default MonitorChart; diff --git a/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx b/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx index f1de5f0bdb83..17c295603f2c 100644 --- a/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx +++ b/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx @@ -150,6 +150,9 @@ const PodLineChart = ({ min: 0, axisLabel: { show: isShowLabel + }, + splitLine: { + show: false } }, grid: { diff --git a/frontend/providers/applaunchpad/src/components/app/detail/index/AdvancedInfo.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/AdvancedInfo.tsx new file mode 100644 index 000000000000..cfabf6fea917 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/app/detail/index/AdvancedInfo.tsx @@ -0,0 +1,314 @@ +import MyIcon from '@/components/Icon'; +import { MOCK_APP_DETAIL } from '@/mock/apps'; +import type { AppDetailType } from '@/types/app'; +import { useCopyData } from '@/utils/tools'; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Center, + Divider, + Flex, + Text, + useTheme +} from '@chakra-ui/react'; +import { MyTooltip } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; +import dynamic from 'next/dynamic'; +import React, { useState } from 'react'; +import styles from '@/components/app/detail/index/index.module.scss'; + +const ConfigMapDetailModal = dynamic(() => import('./ConfigMapDetailModal')); + +const AdvancedInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { copyData } = useCopyData(); + const [detailConfigMap, setDetailConfigMap] = useState<{ + mountPath: string; + value: string; + }>(); + const [isExpanded, setIsExpanded] = useState(false); + + return ( + + setIsExpanded(expandedIndex === 0)}> + + + + {t('Advanced Configuration')} + + + + + {t('Command')}: {app.runCMD || 'Not Configured'} + + + + {t('Environment Variables')}: {app.envs?.length} + + + ConfigMaps: {app.configMapList?.length} + + + {t('Storage')}: {app.storeList?.length} + + + + + + + + + + + + {t('Command')} + + {[ + { label: 'Command', value: app.runCMD || 'Not Configured' }, + { label: 'Parameters', value: app.cmdParam || 'Not Configured' } + ].map((item) => ( + + + {t(item.label)} + + {item.value} + + ))} + + + + {t('Environment Variables')} + + {app.envs?.length > 0 ? ( + + {app.envs.map((env, index) => { + const valText = env.value + ? env.value + : env.valueFrom + ? 'value from | ***' + : ''; + return ( + + + {env.key} + + + copyData(valText)} + > + {valText} + + + + ); + })} + + ) : ( +
+ {t('no_data_available')} +
+ )} +
+
+
+ + + {t('Configuration File')} + + {app.configMapList?.length > 0 ? ( + + {app.configMapList.map((item) => ( + + + + + {item.mountPath} + + + {item.value} + + + + ))} + + ) : ( +
+ {t('no_data_available')} +
+ )} +
+
+ + {t('Storage')} + + {app.storeList?.length > 0 ? ( + + {app.storeList.map((item) => ( + + + + + {item.path} + + + {item.value} Gi + + + + ))} + + ) : ( +
+ {t('no_data_available')} +
+ )} +
+
+
+
+
+
+
+ {detailConfigMap && ( + setDetailConfigMap(undefined)} /> + )} +
+ ); +}; + +export default React.memo(AdvancedInfo); diff --git a/frontend/providers/applaunchpad/src/components/app/detail/index/AppBaseInfo.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/AppBaseInfo.tsx new file mode 100644 index 000000000000..f7f86921d9a1 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/app/detail/index/AppBaseInfo.tsx @@ -0,0 +1,224 @@ +import GPUItem from '@/components/GPUItem'; +import MyIcon from '@/components/Icon'; +import { MOCK_APP_DETAIL } from '@/mock/apps'; +import { useUserStore } from '@/store/user'; +import type { AppDetailType } from '@/types/app'; +import { printMemory, useCopyData } from '@/utils/tools'; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Divider, + Flex, + Tag, + Text, + useTheme +} from '@chakra-ui/react'; +import { MyTooltip } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; +import dynamic from 'next/dynamic'; +import React, { useMemo, useState } from 'react'; +import { sealosApp } from 'sealos-desktop-sdk/app'; + +const ConfigMapDetailModal = dynamic(() => import('./ConfigMapDetailModal')); + +const AppBaseInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { copyData } = useCopyData(); + const { userSourcePrice } = useUserStore(); + const [detailConfigMap, setDetailConfigMap] = useState<{ + mountPath: string; + value: string; + }>(); + + const appInfoTable = useMemo< + { + name: string; + iconName: string; + items: { + label: string; + value?: string; + copy?: string; + render?: React.ReactNode; + }[]; + }[] + >( + () => [ + { + name: 'Basic Information', + iconName: 'formInfo', + items: [ + { label: 'Creation Time', value: app.createTime }, + { + label: `${t('Image Name')} ${app.secret.use ? '(Private)' : ''}`, + value: app.imageName + }, + { label: 'Limit CPU', value: `${app.cpu / 1000} Core` }, + { + label: 'Limit Memory', + value: printMemory(app.memory) + }, + ...(userSourcePrice?.gpu + ? [ + { + label: 'GPU', + render: + } + ] + : []) + ] + }, + { + name: 'Deployment Mode', + iconName: 'deployMode', + items: app.hpa.use + ? [ + { + label: `${app.hpa.target} ${t('target_value')}`, + value: `${app.hpa.value}${app.hpa.target === 'gpu' ? '' : '%'}` + }, + { + label: 'Number of Instances', + value: `${app.hpa.minReplicas} ~ ${app.hpa.maxReplicas}` + } + ] + : [{ label: `Number of Instances`, value: `${app.replicas}` }] + } + ], + [app] + ); + + const appTags = useMemo( + () => [ + ...(app.networks.find((item) => item.openPublicDomain) ? ['Public Access'] : []), + ...(app.hpa.use ? ['Auto scaling'] : ['Fixed instance']), + ...(app.storeList.length > 0 ? ['Stateful'] : ['Stateless']) + ], + [app] + ); + + const persistentVolumes = useMemo(() => { + return app.volumes + .filter((item) => 'persistentVolumeClaim' in item) + .reduce( + ( + acc: { + path: string; + name: string; + }[], + volume + ) => { + const mount = app.volumeMounts.find((m) => m.name === volume.name); + if (mount) { + acc.push({ + path: mount.mountPath, + name: volume.name + }); + } + return acc; + }, + [] + ); + }, [app.volumes, app.volumeMounts]); + + return ( + + {appInfoTable.map((info, index) => ( + + + {t(info.name)} + + + {app?.source?.hasSource && index === 0 && ( + + { + if (!app?.source?.sourceName) return; + if (app.source.sourceType === 'app_store') { + sealosApp.runEvents('openDesktopApp', { + appKey: 'system-template', + pathname: '/instance', + query: { instanceName: app.source.sourceName } + }); + } + if (app.source.sourceType === 'sealaf') { + sealosApp.runEvents('openDesktopApp', { + appKey: 'system-sealaf', + pathname: '/', + query: { instanceName: app.source.sourceName } + }); + } + }} + > + + {t('application_source')} + + + {t(app.source?.sourceType)} + + {t('Manage all resources')} + + + + + )} + {info.items.map((item, i) => ( + + + {t(item.label)} + + + + item.value && !!item.copy && copyData(item.copy)} + > + {item.render ? item.render : item.value} + + + + + ))} + + {index !== appInfoTable.length - 1 && } + + ))} + + {detailConfigMap && ( + setDetailConfigMap(undefined)} /> + )} + + ); +}; + +export default React.memo(AppBaseInfo); diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/AppMainInfo.tsx similarity index 75% rename from frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx rename to frontend/providers/applaunchpad/src/components/app/detail/index/AppMainInfo.tsx index 53efbbae50b1..aa9a95778c88 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx +++ b/frontend/providers/applaunchpad/src/components/app/detail/index/AppMainInfo.tsx @@ -8,7 +8,7 @@ import { DOMAIN_PORT } from '@/store/static'; import type { AppDetailType } from '@/types/app'; import { useCopyData } from '@/utils/tools'; import { getUserNamespace } from '@/utils/user'; -import { Box, Button, Center, Flex, Grid, useDisclosure } from '@chakra-ui/react'; +import { Box, Button, Center, Flex, Grid, Text, useDisclosure } from '@chakra-ui/react'; import dayjs from 'dayjs'; import { useTranslation } from 'next-i18next'; import { useMemo } from 'react'; @@ -35,41 +35,29 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { ); return ( - + <> - - - - {t('Real-time Monitoring')} - - - ({t('Update Time')}  - {dayjs().format('HH:mm')}) + + {t('Real-time Monitoring')} + + ({t('Update Time')} {dayjs().format('HH:mm')}) - CPU ({app.usedCpu.yData[app.usedCpu.yData.length - 1]}%) @@ -85,17 +73,11 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { - - - - {t('Network Configuration')}({networks.length}) - + + {t('Network Configuration')} + + ({networks.length}) + @@ -117,6 +99,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { copyData(network.inline)} @@ -133,6 +116,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { placement={'bottom-start'} > { {!!network.public && ( - copyData(network.public)} - /> + cursor={'pointer'} + > + copyData(network.public)} + /> + )} diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/ConfigMapDetailModal.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/ConfigMapDetailModal.tsx similarity index 100% rename from frontend/providers/applaunchpad/src/pages/app/detail/components/ConfigMapDetailModal.tsx rename to frontend/providers/applaunchpad/src/components/app/detail/index/ConfigMapDetailModal.tsx diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/DelModal.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/DelModal.tsx similarity index 100% rename from frontend/providers/applaunchpad/src/pages/app/detail/components/DelModal.tsx rename to frontend/providers/applaunchpad/src/components/app/detail/index/DelModal.tsx diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/Header.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/Header.tsx similarity index 85% rename from frontend/providers/applaunchpad/src/pages/app/detail/components/Header.tsx rename to frontend/providers/applaunchpad/src/components/app/detail/index/Header.tsx index a2b9910268ab..cb529906062c 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/Header.tsx +++ b/frontend/providers/applaunchpad/src/components/app/detail/index/Header.tsx @@ -112,19 +112,20 @@ const Header = ({ }, [appName, refetch, toast]); return ( - +
router.replace('/apps')}>
- + {appName} - {!isLargeScreen && ( + {/* {!isLargeScreen && ( - )} + )} */} {/* btns */} {isPause ? ( ) : ( )}
@@ -269,6 +261,12 @@ const Pods = ({ fontSize={'12px'} fontWeight={'500'} color={'grayModern.600'} + _first={{ + borderLeftRadius: '6px' + }} + _last={{ + borderRightRadius: '6px' + }} > {t(item.title)} @@ -279,7 +277,7 @@ const Pods = ({ {pods.map((app, i) => ( {columns.map((col) => ( -
+ {col.render ? col.render(app, i) : col.dataIndex @@ -293,7 +291,6 @@ const Pods = ({
- {logsPodIndex !== undefined && ( pod.status.value === PodStatusEnum.running) .map((item, i) => ({ - alias: `${appName}-${i + 1}`, + alias: item.podName, podName: item.podName }))} - podAlias={`${appName}-${logsPodIndex + 1}`} + podAlias={pods[logsPodIndex]?.podName || ''} setLogsPodName={(name: string) => setLogsPodIndex(pods.findIndex((item) => item.podName === name)) } @@ -314,9 +311,9 @@ const Pods = ({ {detailPodIndex !== undefined && ( ({ - alias: `${appName}-${i + 1}`, + alias: item.podName, podName: item.podName }))} setPodDetail={(e: string) => @@ -331,9 +328,9 @@ const Pods = ({ isOpen={isOpenPodFile} onClose={onClosePodFile} pod={pods[detailFilePodIndex]} - podAlias={`${appName}-${detailFilePodIndex + 1}`} + podAlias={pods[detailFilePodIndex]?.podName || ''} pods={pods.map((item, i) => ({ - alias: `${appName}-${i + 1}`, + alias: item.podName, podName: item.podName }))} setPodDetail={(e: string) => diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/UpdateModal.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/UpdateModal.tsx similarity index 100% rename from frontend/providers/applaunchpad/src/pages/app/detail/components/UpdateModal.tsx rename to frontend/providers/applaunchpad/src/components/app/detail/index/UpdateModal.tsx diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/empty.module.scss b/frontend/providers/applaunchpad/src/components/app/detail/index/empty.module.scss similarity index 100% rename from frontend/providers/applaunchpad/src/pages/app/detail/components/empty.module.scss rename to frontend/providers/applaunchpad/src/components/app/detail/index/empty.module.scss diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/empty.tsx b/frontend/providers/applaunchpad/src/components/app/detail/index/empty.tsx similarity index 100% rename from frontend/providers/applaunchpad/src/pages/app/detail/components/empty.tsx rename to frontend/providers/applaunchpad/src/components/app/detail/index/empty.tsx diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/index.module.scss b/frontend/providers/applaunchpad/src/components/app/detail/index/index.module.scss similarity index 100% rename from frontend/providers/applaunchpad/src/pages/app/detail/index.module.scss rename to frontend/providers/applaunchpad/src/components/app/detail/index/index.module.scss diff --git a/frontend/providers/applaunchpad/src/components/app/detail/logs/Filter.tsx b/frontend/providers/applaunchpad/src/components/app/detail/logs/Filter.tsx new file mode 100644 index 000000000000..6c7e28a3befe --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/app/detail/logs/Filter.tsx @@ -0,0 +1,228 @@ +import MyIcon from '@/components/Icon'; +import { JsonFilterItem, LogsFormData } from '@/pages/app/detail/logs'; +import { Button, ButtonProps, Center, Flex, Input, Switch, Text } from '@chakra-ui/react'; +import { MySelect } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; +import { UseFormReturn, useFieldArray } from 'react-hook-form'; + +export const Filter = ({ + formHook, + refetchData +}: { + formHook: UseFormReturn; + refetchData: () => void; +}) => { + const { t } = useTranslation(); + const [activeId, setActiveId] = useState('normal_filter'); + const [inputKeyword, setInputKeyword] = useState(formHook.watch('keyword')); + + const isJsonMode = formHook.watch('isJsonMode'); + const isOnlyStderr = formHook.watch('isOnlyStderr'); + const filterKeys = formHook.watch('filterKeys'); + + const { fields, append, remove } = useFieldArray({ + control: formHook.control, + name: 'jsonFilters' + }); + + return ( + + {/* tab */} + {/* + + */} + {/* operator button */} + + + + {t('json_mode')} + + { + formHook.setValue('isJsonMode', !isJsonMode); + formHook.setValue('jsonFilters', []); + }} + /> + + + + {t('only_stderr')} + + formHook.setValue('isOnlyStderr', !isOnlyStderr)} + /> + + + setInputKeyword(e.target.value)} + /> + + + + + {/* json mode */} + {isJsonMode && ( + + {filterKeys.length > 0 || fields.length > 0 ? ( + + append({ + key: '', + value: '', + mode: '=' + }) + } + /> + ) : ( +
+ + {t('no_data_available')} + +
+ )} + + {fields.map((field, index) => ( + + formHook.setValue(`jsonFilters.${index}.key`, val)} + /> + + formHook.setValue(`jsonFilters.${index}.mode`, val as JsonFilterItem['mode']) + } + /> + formHook.setValue(`jsonFilters.${index}.value`, e.target.value)} + border={'1px solid #E8EBF0'} + boxShadow={ + '0px 1px 2px 0px rgba(19, 51, 107, 0.05),0px 0px 1px 0px rgba(19, 51, 107, 0.08)' + } + /> + + {index === fields.length - 1 && ( + + append({ + key: '', + value: '', + mode: '=' + }) + } + /> + )} + + ))} +
+ )} +
+ ); +}; + +const AppendJSONFormItemButton = (props: ButtonProps) => { + const { t } = useTranslation(); + return ( + + ); +}; diff --git a/frontend/providers/applaunchpad/src/components/app/detail/logs/Header.tsx b/frontend/providers/applaunchpad/src/components/app/detail/logs/Header.tsx new file mode 100644 index 000000000000..00867deb7c0b --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/app/detail/logs/Header.tsx @@ -0,0 +1,204 @@ +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + ButtonGroup, + Flex, + Grid, + Input, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, + useMediaQuery +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import dynamic from 'next/dynamic'; + +import AdvancedSelect from '@/components/AdvancedSelect'; +import MyIcon from '@/components/Icon'; +import { REFRESH_INTERVAL_OPTIONS } from '@/constants/monitor'; +import { LogsFormData } from '@/pages/app/detail/logs'; +import useDateTimeStore from '@/store/date'; +import { UseFormReturn } from 'react-hook-form'; +import { MyTooltip } from '@sealos/ui'; + +const DatePicker = dynamic(() => import('@/components/DatePicker'), { ssr: false }); + +export const Header = ({ + formHook, + refetchData +}: { + formHook: UseFormReturn; + refetchData: () => void; +}) => { + const { t } = useTranslation(); + const { refreshInterval, setRefreshInterval } = useDateTimeStore(); + const [isLargerThan1440] = useMediaQuery('(min-width: 1440px)'); + + return ( + + + + + {t('time')} + + + + + + Pods + + } + width={'fit-content'} + value={'hello-sql-postgresql-0'} + onCheckboxChange={(val) => { + formHook.setValue('pods', val); + }} + list={formHook.watch('pods')} + /> + + + + + + Containers + + } + value={'hello-sql-postgresql-0'} + list={formHook.watch('containers')} + onCheckboxChange={(val) => { + formHook.setValue('containers', val); + }} + /> + + + + {t('log_number')} + + { + const val = Number(e.target.value); + if (isNaN(val)) { + formHook.setValue('limit', 1); + } else if (val > 500) { + formHook.setValue('limit', 500); + } else if (val < 1) { + formHook.setValue('limit', 1); + } else { + formHook.setValue('limit', val); + } + }} + /> + + + + + + + {refreshInterval === 0 ? null : ( + {`${refreshInterval / 1000}s`} + )} + + + + + {REFRESH_INTERVAL_OPTIONS.map((item) => ( + { + setRefreshInterval(item.value); + }} + {...(refreshInterval === item.value + ? { + color: 'brightBlue.600' + } + : {})} + borderRadius={'4px'} + _hover={{ + bg: 'rgba(17, 24, 36, 0.05)', + color: 'brightBlue.600' + }} + p={'6px'} + > + {item.label} + + ))} + + + + + + + ); +}; diff --git a/frontend/providers/applaunchpad/src/components/app/detail/logs/LogCounts.tsx b/frontend/providers/applaunchpad/src/components/app/detail/logs/LogCounts.tsx new file mode 100644 index 000000000000..5a963906da8d --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/app/detail/logs/LogCounts.tsx @@ -0,0 +1,85 @@ +import MyIcon from '@/components/Icon'; +import LogBarChart from '@/components/LogBarChart'; +import { Box, Button, Center, Collapse, Flex, Spinner, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; +import EmptyChart from '@/components/Icon/icons/emptyChart.svg'; + +export const LogCounts = ({ + logCountsData, + isLogCountsLoading +}: { + logCountsData: { logs_total: string; _time: string }[]; + isLogCountsLoading?: boolean; +}) => { + const { t } = useTranslation(); + const [onOpenChart, setOnOpenChart] = useState(true); + + const processChartData = (rawData: Array<{ _time: string; logs_total: string }>) => { + const sortedData = [...rawData].sort( + (a, b) => new Date(a._time).getTime() - new Date(b._time).getTime() + ); + const xData = sortedData.map((item) => Math.floor(new Date(item._time).getTime() / 1000)); + const yData = sortedData.map((item) => item.logs_total); + + return { + xData, + yData + }; + }; + + return ( + + + + + {/* charts */} + + + {isLogCountsLoading ? ( +
+ +
+ ) : logCountsData.length > 0 ? ( + + ) : ( +
+ + + {t('no_data_available')} + +
+ )} +
+
+
+ ); +}; diff --git a/frontend/providers/applaunchpad/src/components/app/detail/logs/LogTable.tsx b/frontend/providers/applaunchpad/src/components/app/detail/logs/LogTable.tsx new file mode 100644 index 000000000000..500196eb0f56 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/app/detail/logs/LogTable.tsx @@ -0,0 +1,305 @@ +import { BaseTable } from '@/components/BaseTable/index'; +import { + Box, + Button, + Checkbox, + CheckboxGroup, + Collapse, + Divider, + Flex, + Text +} from '@chakra-ui/react'; +import { + ColumnDef, + getCoreRowModel, + getFilteredRowModel, + useReactTable +} from '@tanstack/react-table'; +import { get } from 'lodash'; +import { useTranslation } from 'next-i18next'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import MyIcon from '@/components/Icon'; +import { formatTime } from '@/utils/tools'; +import { LogsFormData } from '@/pages/app/detail/logs'; +import { UseFormReturn } from 'react-hook-form'; +import { useLogStore } from '@/store/logStore'; + +interface FieldItem { + value: string; + label: string; + checked: boolean; + accessorKey: string; +} + +interface LogData { + stream: string; + [key: string]: any; +} + +export const LogTable = ({ + data, + isLoading, + formHook +}: { + data: any[]; + isLoading: boolean; + formHook: UseFormReturn; +}) => { + const { t, i18n } = useTranslation(); + const lang = i18n.language; + + const [onOpenField, setOnOpenField] = useState(false); + const [hiddenFieldCount, setHiddenFieldCount] = useState(0); + const [visibleFieldCount, setVisibleFieldCount] = useState(0); + const isJsonMode = formHook.watch('isJsonMode'); + const { exportLogs } = useLogStore(); + + const generateFieldList = useCallback((data: any[], prevFieldList: FieldItem[] = []) => { + if (!data.length) return []; + + const uniqueKeys = new Set(); + data.forEach((item) => { + Object.keys(item).forEach((key) => { + uniqueKeys.add(key); + }); + }); + + const prevFieldStates = prevFieldList.reduce((acc, field) => { + acc[field.value] = field.checked; + return acc; + }, {} as Record); + + return Array.from(uniqueKeys).map((key) => ({ + value: key, + label: key, + checked: key in prevFieldStates ? prevFieldStates[key] : true, + accessorKey: key + })); + }, []); + + const [fieldList, setFieldList] = useState([]); + + useEffect(() => { + setFieldList((prevFieldList) => generateFieldList(data, prevFieldList)); + const excludeFields = ['_time', '_msg', 'container', 'pod', 'stream']; + formHook.setValue( + 'filterKeys', + generateFieldList(data) + .filter((field) => !excludeFields.includes(field.value)) + .map((field) => ({ value: field.value, label: field.label })) + ); + }, [data, generateFieldList, formHook]); + + useEffect(() => { + const visibleCount = fieldList.filter((field) => field.checked).length; + setVisibleFieldCount(visibleCount); + setHiddenFieldCount(fieldList.length - visibleCount); + }, [fieldList]); + + const columns = useMemo>>(() => { + return fieldList + .filter((field) => field.checked) + .map((field) => ({ + accessorKey: field.accessorKey, + header: () => { + if (field.label === '_time' || field.label === '_msg') { + return field.label.substring(1); + } + return field.label; + }, + cell: ({ row }) => { + let value = get(row.original, field.accessorKey, ''); + + if (field.accessorKey === '_time') { + value = formatTime(value, 'YYYY-MM-DD HH:mm:ss'); + } + + return ( + + {value?.toString() || ''} + + ); + }, + meta: { + isError: (row: any) => row.stream === 'stderr' + } + })); + }, [fieldList]); + + const table = useReactTable({ + data: data, + columns, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel() + }); + + return ( + + + + + {t('Log')} + + {isJsonMode && ( + + + + + {t('visible')}: + + + {visibleFieldCount} {lang === 'zh' ? t('piece') : ''} + + + + + + + + {t('hidden')}: + + + {hiddenFieldCount} {lang === 'zh' ? t('piece') : ''} + + + + )} + + + + + {isJsonMode && ( + + + + {fieldList.map((item) => ( + + setFieldList( + fieldList.map((field) => + field.value === item.value ? { ...field, checked: !field.checked } : field + ) + ) + } + sx={{ + 'span.chakra-checkbox__control[data-checked]': { + background: '#f0f4ff ', + border: '1px solid #219bf4 ', + boxShadow: '0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)', + color: '#219bf4', + borderRadius: '4px' + } + }} + > + {item.label} + + ))} + + + + )} + + {data.length > 0 ? ( + + ) : ( + + + + {t('no_data_available')} + + + )} + + ); +}; diff --git a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx b/frontend/providers/applaunchpad/src/components/apps/appList.tsx similarity index 98% rename from frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx rename to frontend/providers/applaunchpad/src/components/apps/appList.tsx index 7acda1631c36..8688e35ada65 100644 --- a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx +++ b/frontend/providers/applaunchpad/src/components/apps/appList.tsx @@ -26,9 +26,9 @@ import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; import React, { useCallback, useMemo, useState } from 'react'; import type { ThemeType } from '@sealos/ui'; -import UpdateModal from '@/pages/app/detail/components/UpdateModal'; +import UpdateModal from '@/components/app/detail/index/UpdateModal'; -const DelModal = dynamic(() => import('@/pages/app/detail/components/DelModal')); +const DelModal = dynamic(() => import('@/components/app/detail/index/DelModal')); const AppList = ({ apps = [], diff --git a/frontend/providers/applaunchpad/src/pages/apps/components/empty.module.scss b/frontend/providers/applaunchpad/src/components/apps/empty.module.scss similarity index 100% rename from frontend/providers/applaunchpad/src/pages/apps/components/empty.module.scss rename to frontend/providers/applaunchpad/src/components/apps/empty.module.scss diff --git a/frontend/providers/applaunchpad/src/pages/apps/components/empty.tsx b/frontend/providers/applaunchpad/src/components/apps/empty.tsx similarity index 100% rename from frontend/providers/applaunchpad/src/pages/apps/components/empty.tsx rename to frontend/providers/applaunchpad/src/components/apps/empty.tsx diff --git a/frontend/providers/applaunchpad/src/components/layouts/DetailLayout.tsx b/frontend/providers/applaunchpad/src/components/layouts/DetailLayout.tsx new file mode 100644 index 000000000000..5bf271955fd1 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/layouts/DetailLayout.tsx @@ -0,0 +1,77 @@ +import Sidebar, { ROUTES } from '@/components/layouts/Sidebar'; +import { useToast } from '@/hooks/useToast'; +import { MOCK_APP_DETAIL } from '@/mock/apps'; +import Header from '@/components/app/detail/index/Header'; +import { useAppStore } from '@/store/app'; +import { useGlobalStore } from '@/store/global'; +import { Flex } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import React, { useMemo, useState } from 'react'; +import { useRouter } from 'next/router'; + +interface DetailLayoutProps { + children: React.ReactNode; + appName: string; +} + +export default function DetailLayout({ children, appName }: DetailLayoutProps) { + const { toast } = useToast(); + const router = useRouter(); + const { screenWidth } = useGlobalStore(); + const isLargeScreen = useMemo(() => screenWidth > 1280, [screenWidth]); + + const { + appDetail = MOCK_APP_DETAIL, + setAppDetail, + intervalLoadPods, + loadDetailMonitorData + } = useAppStore(); + + const [showSlider, setShowSlider] = useState(false); + + const { refetch } = useQuery(['setAppDetail'], () => setAppDetail(appName), { + onError(err) { + toast({ + title: String(err), + status: 'error' + }); + } + }); + + useQuery( + ['app-detail-pod'], + () => { + if (appDetail?.isPause) return null; + return intervalLoadPods(appName, true); + }, + { + refetchOnMount: true, + refetchInterval: router.pathname === ROUTES.OVERVIEW ? 3000 : 5000, + staleTime: router.pathname === ROUTES.OVERVIEW ? 3000 : 5000 + } + ); + + return ( + +
+ + + {children} + + + ); +} diff --git a/frontend/providers/applaunchpad/src/components/layouts/Sidebar.tsx b/frontend/providers/applaunchpad/src/components/layouts/Sidebar.tsx new file mode 100644 index 000000000000..e0e855658271 --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/layouts/Sidebar.tsx @@ -0,0 +1,91 @@ +import { Center, Text, Stack } from '@chakra-ui/react'; +import MyIcon from '../Icon'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; + +export const ROUTES = { + OVERVIEW: '/app/detail', + MONITOR: '/app/detail/monitor', + LOGS: '/app/detail/logs' +} as const; + +export default function Sidebar() { + const { t } = useTranslation(); + const router = useRouter(); + + const siderbarMap = [ + { + label: t('overview'), + icon: ( + + ), + path: ROUTES.OVERVIEW + }, + { + label: t('monitor'), + icon: ( + + ), + path: ROUTES.MONITOR + }, + { + label: t('Log'), + icon: ( + + ), + path: ROUTES.LOGS + } + ]; + + return ( + + {siderbarMap.map((item) => ( +
{ + console.log(router.query); + router.push({ + pathname: item.path, + query: { ...router.query } + }); + }} + > + {item.icon} + + {item.label} + +
+ ))} +
+ ); +} diff --git a/frontend/providers/applaunchpad/src/constants/editApp.ts b/frontend/providers/applaunchpad/src/constants/editApp.ts index 9b19828fbfcd..6cf0e2a19157 100644 --- a/frontend/providers/applaunchpad/src/constants/editApp.ts +++ b/frontend/providers/applaunchpad/src/constants/editApp.ts @@ -23,6 +23,7 @@ export const editModeMap = (isEdit: boolean) => { }; export const defaultEditVal: AppEditType = { + kind: 'deployment', appName: 'hello-world', imageName: 'nginx', runCMD: '', @@ -58,6 +59,8 @@ export const defaultEditVal: AppEditType = { serverAddress: 'docker.io' }, storeList: [], + volumes: [], + volumeMounts: [], gpu: { manufacturers: 'nvidia', type: '', diff --git a/frontend/providers/applaunchpad/src/constants/monitor.ts b/frontend/providers/applaunchpad/src/constants/monitor.ts index 713e057f8b57..30faa0e1ccfa 100644 --- a/frontend/providers/applaunchpad/src/constants/monitor.ts +++ b/frontend/providers/applaunchpad/src/constants/monitor.ts @@ -1,36 +1,42 @@ export const LineStyleMap = [ { - backgroundColor: '#EBF5FB', - lineColor: '#5EBDF2' + backgroundColor: 'rgba(209, 244, 255, 0.3)', + lineColor: '#11B6FC' }, { - backgroundColor: 'rgba(241, 240, 249, 1)', - lineColor: 'rgba(154, 142, 224, 1)' + backgroundColor: 'rgba(255, 221, 252, 0.3)', + lineColor: '#8774EE' }, { - backgroundColor: 'rgba(237, 247, 247, 1)', - lineColor: 'rgba(108, 211, 204, 1)' + backgroundColor: 'rgba(254, 206, 255, 0.3)', + lineColor: '#C172E7' }, - { - backgroundColor: 'rgba(250, 239, 244, 1)', - lineColor: 'rgba(241, 130, 170, 1)' + backgroundColor: 'rgba(199, 255, 248, 0.3)', + lineColor: '#13C4B9' }, { - backgroundColor: 'rgba(108, 211, 204, 0.1)', - lineColor: 'rgba(108, 211, 204, 1)' + backgroundColor: 'rgba(255, 224, 235, 0.3)', + lineColor: '#FF81AE' }, { - backgroundColor: 'rgba(250, 239, 244, 1)', - lineColor: 'rgba(252, 150, 99, 1)' + backgroundColor: 'rgba(255, 238, 231, 0.3)', + lineColor: '#FB6514' }, - { - backgroundColor: 'rgba(251, 235, 238, 1)', - lineColor: 'rgba(255, 91, 110, 1)' + backgroundColor: 'rgba(255, 224, 224, 0.05)', + lineColor: '#F04438' }, { - backgroundColor: 'rgba(249, 248, 234, 1)', - lineColor: 'rgba(236, 218, 70, 1)' + backgroundColor: 'rgba(241, 255, 185, 0.3)', + lineColor: '#E7D435' } ]; + +export const REFRESH_INTERVAL_OPTIONS = [ + { value: 0, label: 'close' }, + { value: 1000, label: '1s' }, + { value: 2000, label: '2s' }, + { value: 5000, label: '5s' }, + { value: 10000, label: '10s' } +]; diff --git a/frontend/providers/applaunchpad/src/constants/theme.ts b/frontend/providers/applaunchpad/src/constants/theme.ts index 482ab43103ee..c48ece760177 100644 --- a/frontend/providers/applaunchpad/src/constants/theme.ts +++ b/frontend/providers/applaunchpad/src/constants/theme.ts @@ -23,7 +23,8 @@ export const theme = extendTheme(sealosTheme, { 'html, body': { fontSize: 'md', height: '100%', - overflow: 'overlay', + backgroundColor: '#F4F4F7', + overflowX: 'auto', fontWeight: 400, minWidth: '1024px' } diff --git a/frontend/providers/applaunchpad/src/hooks/useConfirm.tsx b/frontend/providers/applaunchpad/src/hooks/useConfirm.tsx index 02174ab4b4cb..18ddc98f0104 100644 --- a/frontend/providers/applaunchpad/src/hooks/useConfirm.tsx +++ b/frontend/providers/applaunchpad/src/hooks/useConfirm.tsx @@ -31,7 +31,12 @@ export const useConfirm = ({ title = 'Warning', content }: { title?: string; con ), ConfirmChild: useCallback( () => ( - + diff --git a/frontend/providers/applaunchpad/src/hooks/useRequest.tsx b/frontend/providers/applaunchpad/src/hooks/useRequest.tsx index b491a1abf384..ca2a33d354e6 100644 --- a/frontend/providers/applaunchpad/src/hooks/useRequest.tsx +++ b/frontend/providers/applaunchpad/src/hooks/useRequest.tsx @@ -2,7 +2,7 @@ import { useToast } from '@/hooks/useToast'; import { useMutation } from '@tanstack/react-query'; import type { UseMutationOptions } from '@tanstack/react-query'; import { getErrText } from '@/utils/tools'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; interface Props extends UseMutationOptions { successToast?: string | null; diff --git a/frontend/providers/applaunchpad/src/mock/apps.ts b/frontend/providers/applaunchpad/src/mock/apps.ts index f6809b3a7449..af76efe453a2 100644 --- a/frontend/providers/applaunchpad/src/mock/apps.ts +++ b/frontend/providers/applaunchpad/src/mock/apps.ts @@ -238,6 +238,9 @@ export const MOCK_PODS: PodDetailType[] = [ ]; export const MOCK_APP_DETAIL: AppDetailType = { + kind: 'deployment', + volumes: [], + volumeMounts: [], crYamlList: [], id: '4bd50c41-149e-4da5-89d5-0308b9dd75c6', createTime: '2022/1/22', diff --git a/frontend/providers/applaunchpad/src/pages/_app.tsx b/frontend/providers/applaunchpad/src/pages/_app.tsx index 3f4e5f253145..61db547f2d47 100644 --- a/frontend/providers/applaunchpad/src/pages/_app.tsx +++ b/frontend/providers/applaunchpad/src/pages/_app.tsx @@ -12,15 +12,14 @@ import { appWithTranslation, useTranslation } from 'next-i18next'; import type { AppProps } from 'next/app'; import Head from 'next/head'; import Router, { useRouter } from 'next/router'; -import NProgress from 'nprogress'; //nprogress module +import NProgress from 'nprogress'; import { useEffect, useState } from 'react'; import { EVENT_NAME } from 'sealos-desktop-sdk'; import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app'; +import 'react-day-picker/dist/style.css'; import '@/styles/reset.scss'; import 'nprogress/nprogress.css'; import '@sealos/driver/src/driver.css'; -import { AppEditSyncedFields } from '@/types/app'; -import Script from 'next/script'; //Binding events. Router.events.on('routeChangeStart', () => NProgress.start()); diff --git a/frontend/providers/applaunchpad/src/pages/api/log/queryLogs.ts b/frontend/providers/applaunchpad/src/pages/api/log/queryLogs.ts new file mode 100644 index 000000000000..2f96f174c983 --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/api/log/queryLogs.ts @@ -0,0 +1,108 @@ +import { JsonFilterItem } from '@/pages/app/detail/logs'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; + +import type { NextApiRequest, NextApiResponse } from 'next'; + +export interface LogQueryPayload { + app?: string; + time?: string; + namespace?: string; + limit?: string; + jsonMode?: string; + stderrMode?: string; + numberMode?: string; + numberLevel?: string; + pod?: string[]; + container?: string[]; + keyword?: string; + jsonQuery?: JsonFilterItem[]; + exportMode?: boolean; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const logUrl = global.AppConfig.launchpad.components.log.url; + + if (!logUrl) { + return jsonRes(res, { + code: 400, + error: 'logUrl is not set' + }); + } + + if (req.method !== 'POST') { + return jsonRes(res, { + code: 405, + error: 'Method not allowed' + }); + } + + try { + const kubeconfig = await authSession(req.headers); + const { namespace } = await getK8s({ + kubeconfig: kubeconfig + }); + + if (!req.body.app) { + return jsonRes(res, { + code: 400, + error: 'app is required' + }); + } + + const { + time = '30d', + app = '', + limit = '10', + jsonMode = 'true', + stderrMode = 'false', + numberMode = 'false', + numberLevel = '', + pod = [], + container = [], + keyword = '', + jsonQuery = [], + exportMode = false + } = req.body as LogQueryPayload; + + const params: LogQueryPayload = { + time: time, + namespace: namespace, + app: app, + limit: limit, + jsonMode: jsonMode, + stderrMode: stderrMode, + numberMode: numberMode, + ...(numberLevel && { numberLevel: numberLevel }), + pod: Array.isArray(pod) ? pod : [], + container: Array.isArray(container) ? container : [], + keyword: keyword, + jsonQuery: Array.isArray(jsonQuery) ? jsonQuery : [] + }; + + console.log(params, 'params'); + const result = await fetch(logUrl + '/queryLogsByParams', { + method: 'POST', + body: JSON.stringify(params), + headers: { + 'Content-Type': 'application/json', + Authorization: encodeURIComponent(kubeconfig) + } + }); + console.log('fetch /queryLogsByParams: ', result.status); + const data = await result.text(); + + jsonRes(res, { + code: 200, + data: data + }); + } catch (error) { + console.log(error, 'error'); + jsonRes(res, { + code: 500, + error: error + }); + } +} diff --git a/frontend/providers/applaunchpad/src/pages/api/log/queryPodList.ts b/frontend/providers/applaunchpad/src/pages/api/log/queryPodList.ts new file mode 100644 index 000000000000..554c9308233a --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/api/log/queryPodList.ts @@ -0,0 +1,84 @@ +import { JsonFilterItem } from '@/pages/app/detail/logs'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; + +import type { NextApiRequest, NextApiResponse } from 'next'; + +export interface PodListQueryPayload { + app?: string; + time?: string; + namespace?: string; + podQuery?: string; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const logUrl = global.AppConfig.launchpad.components.log.url; + + if (!logUrl) { + return jsonRes(res, { + code: 400, + error: 'logUrl is not set' + }); + } + + if (req.method !== 'POST') { + return jsonRes(res, { + code: 405, + error: 'Method not allowed' + }); + } + + try { + const kubeconfig = await authSession(req.headers); + const { namespace } = await getK8s({ + kubeconfig: kubeconfig + }); + + if (!req.body.app) { + return jsonRes(res, { + code: 400, + error: 'app is required' + }); + } + + const { time = '30d', app = '', podQuery = 'true' } = req.body as PodListQueryPayload; + + const params: PodListQueryPayload = { + time: time, + namespace: namespace, + app: app, + podQuery: podQuery + }; + + console.log(params, 'params'); + const result = await fetch(logUrl + '/queryPodList', { + method: 'POST', + body: JSON.stringify(params), + headers: { + 'Content-Type': 'application/json', + Authorization: encodeURIComponent(kubeconfig) + } + }); + console.log('fetch /queryPodList: ', result.status); + if (result.status !== 200) { + return jsonRes(res, { + data: [] + }); + } + + const data = await result.json(); + + jsonRes(res, { + code: 200, + data: data + }); + } catch (error) { + console.log(error, 'error'); + jsonRes(res, { + code: 500, + error: error + }); + } +} diff --git a/frontend/providers/applaunchpad/src/pages/api/monitor/getMonitorData.ts b/frontend/providers/applaunchpad/src/pages/api/monitor/getMonitorData.ts index c7c2be4405ee..f1f90746ef74 100644 --- a/frontend/providers/applaunchpad/src/pages/api/monitor/getMonitorData.ts +++ b/frontend/providers/applaunchpad/src/pages/api/monitor/getMonitorData.ts @@ -87,15 +87,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const { queryName, queryKey, start, end, step = '1m' } = req.query; // One hour of monitoring data - const endTime = Date.now(); - const startTime = endTime - 60 * 60 * 1000; + const endTime = end ? Number(end) : Date.now(); + const startTime = start ? Number(start) : endTime - 60 * 60 * 1000; const params = { type: queryKey, launchPadName: queryName, namespace: namespace, - start: startTime / 1000, - end: endTime / 1000, + start: Math.floor(startTime / 1000), + end: Math.floor(endTime / 1000), step: step }; @@ -106,7 +106,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }, kubeconfig ).then((res) => { - // console.log(res.data.result, res.data.result[0].values.length, 'AdapterChartData'); // @ts-ignore return AdapterChartData[queryKey] ? // @ts-ignore diff --git a/frontend/providers/applaunchpad/src/pages/api/pauseApp.ts b/frontend/providers/applaunchpad/src/pages/api/pauseApp.ts index dc90275d7cfd..b7fc2aeafb2d 100644 --- a/frontend/providers/applaunchpad/src/pages/api/pauseApp.ts +++ b/frontend/providers/applaunchpad/src/pages/api/pauseApp.ts @@ -3,7 +3,8 @@ import { ApiResp } from '@/services/kubernet'; import { authSession } from '@/services/backend/auth'; import { getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; -import { pauseKey } from '@/constants/app'; +import { appDeployKey, pauseKey } from '@/constants/app'; +import { PatchUtils } from '@kubernetes/client-node'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -11,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< if (!appName) { throw new Error('appName is empty'); } - const { apiClient, k8sAutoscaling, getDeployApp, namespace } = await getK8s({ + const { apiClient, k8sAutoscaling, getDeployApp, namespace, k8sNetworkingApp } = await getK8s({ kubeconfig: await authSession(req.headers) }); @@ -47,6 +48,57 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } } + // handle ingress + try { + const { body: ingress } = await k8sNetworkingApp.listNamespacedIngress( + namespace, + undefined, + undefined, + undefined, + undefined, + `${appDeployKey}=${appName}` + ); + if (ingress?.items?.length > 0) { + for (const ingressItem of ingress.items) { + if (ingressItem?.metadata?.name) { + const patchData: Record = {}; + if (ingressItem.metadata?.annotations?.['kubernetes.io/ingress.class'] === 'nginx') { + patchData.metadata = { + annotations: { + 'kubernetes.io/ingress.class': 'pause' + } + }; + } + if (ingressItem.spec?.ingressClassName === 'nginx') { + patchData.spec = { + ingressClassName: 'pause' + }; + } + + if (Object.keys(patchData).length > 0) { + requestQueue.push( + k8sNetworkingApp.patchNamespacedIngress( + ingressItem.metadata.name, + namespace, + patchData, + undefined, + undefined, + undefined, + undefined, + undefined, + { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_MERGE_PATCH } } + ) + ); + } + } + } + } + } catch (error: any) { + if (error?.statusCode !== 404) { + return Promise.reject('无法读取到ingress'); + } + } + // replace source file app.metadata.annotations[pauseKey] = JSON.stringify(restartAnnotations); app.spec.replicas = 0; diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts index 0162c5488ff1..e2a052b81929 100644 --- a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts +++ b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts @@ -51,6 +51,9 @@ export const defaultAppConfig: AppConfigType = { }, billing: { url: 'http://account-service.account-system.svc:2333' + }, + log: { + url: 'http://localhost:8080' } }, appResourceFormSliderConfig: { diff --git a/frontend/providers/applaunchpad/src/pages/api/startApp.ts b/frontend/providers/applaunchpad/src/pages/api/startApp.ts index 6784371ee8e7..9c1c18bcd010 100644 --- a/frontend/providers/applaunchpad/src/pages/api/startApp.ts +++ b/frontend/providers/applaunchpad/src/pages/api/startApp.ts @@ -3,9 +3,10 @@ import { ApiResp } from '@/services/kubernet'; import { authSession } from '@/services/backend/auth'; import { getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; -import { pauseKey, minReplicasKey, maxReplicasKey } from '@/constants/app'; +import { pauseKey, minReplicasKey, maxReplicasKey, appDeployKey } from '@/constants/app'; import { json2HPA } from '@/utils/deployYaml2Json'; import { AppEditType } from '@/types/app'; +import { PatchUtils } from '@kubernetes/client-node'; /* start app. */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -14,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< if (!appName) { throw new Error('appName is empty'); } - const { apiClient, getDeployApp, applyYamlList } = await getK8s({ + const { apiClient, getDeployApp, applyYamlList, namespace, k8sNetworkingApp } = await getK8s({ kubeconfig: await authSession(req.headers) }); @@ -60,6 +61,57 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< requestQueue.push(applyYamlList([hpaYaml], 'create')); } + // handle ingress + try { + const { body: ingress } = await k8sNetworkingApp.listNamespacedIngress( + namespace, + undefined, + undefined, + undefined, + undefined, + `${appDeployKey}=${appName}` + ); + if (ingress?.items?.length > 0) { + for (const ingressItem of ingress.items) { + if (ingressItem?.metadata?.name) { + const patchData: Record = {}; + if (ingressItem.metadata?.annotations?.['kubernetes.io/ingress.class'] === 'pause') { + patchData.metadata = { + annotations: { + 'kubernetes.io/ingress.class': 'nginx' + } + }; + } + if (ingressItem.spec?.ingressClassName === 'pause') { + patchData.spec = { + ingressClassName: 'nginx' + }; + } + + if (Object.keys(patchData).length > 0) { + requestQueue.push( + k8sNetworkingApp.patchNamespacedIngress( + ingressItem.metadata.name, + namespace, + patchData, + undefined, + undefined, + undefined, + undefined, + undefined, + { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_MERGE_PATCH } } + ) + ); + } + } + } + } + } catch (error: any) { + if (error?.statusCode !== 404) { + return Promise.reject('无法读取到ingress'); + } + } + await Promise.all(requestQueue); jsonRes(res); diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx deleted file mode 100644 index d5ad98246bba..000000000000 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx +++ /dev/null @@ -1,429 +0,0 @@ -import GPUItem from '@/components/GPUItem'; -import MyIcon from '@/components/Icon'; -import { MOCK_APP_DETAIL } from '@/mock/apps'; -import { useUserStore } from '@/store/user'; -import type { AppDetailType } from '@/types/app'; -import { printMemory, useCopyData } from '@/utils/tools'; -import { - Accordion, - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Box, - Flex, - Tag, - useTheme -} from '@chakra-ui/react'; -import { MyTooltip } from '@sealos/ui'; -import { useTranslation } from 'next-i18next'; -import dynamic from 'next/dynamic'; -import React, { useMemo, useState } from 'react'; -import { sealosApp } from 'sealos-desktop-sdk/app'; -import styles from '../index.module.scss'; - -const ConfigMapDetailModal = dynamic(() => import('./ConfigMapDetailModal')); - -const AppBaseInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const { copyData } = useCopyData(); - const { userSourcePrice } = useUserStore(); - const [detailConfigMap, setDetailConfigMap] = useState<{ - mountPath: string; - value: string; - }>(); - - const appInfoTable = useMemo< - { - name: string; - iconName: string; - items: { - label: string; - value?: string; - copy?: string; - render?: React.ReactNode; - }[]; - }[] - >( - () => [ - { - name: 'Basic Information', - iconName: 'formInfo', - items: [ - { label: 'Creation Time', value: app.createTime }, - { - label: `${t('Image Name')} ${app.secret.use ? '(Private)' : ''}`, - value: app.imageName - }, - { label: 'Limit CPU', value: `${app.cpu / 1000} Core` }, - { - label: 'Limit Memory', - value: printMemory(app.memory) - }, - ...(userSourcePrice?.gpu - ? [ - { - label: 'GPU', - render: - } - ] - : []) - ] - }, - { - name: 'Deployment Mode', - iconName: 'deployMode', - items: app.hpa.use - ? [ - { label: `${app.hpa.target} ${t('target_value')}`, value: `${app.hpa.value}%` }, - { - label: 'Number of Instances', - value: `${app.hpa.minReplicas} ~ ${app.hpa.maxReplicas}` - } - ] - : [{ label: `Number of Instances`, value: `${app.replicas}` }] - } - ], - [app] - ); - - const appTags = useMemo( - () => [ - ...(app.networks.find((item) => item.openPublicDomain) ? ['Public Access'] : []), - ...(app.hpa.use ? ['Auto scaling'] : ['Fixed instance']), - ...(app.storeList.length > 0 ? ['Stateful'] : ['Stateless']) - ], - [app] - ); - - return ( - - {app?.source?.hasSource && ( - - - - {t('Application Source')} - - - { - if (!app?.source?.sourceName) return; - if (app.source.sourceType === 'app_store') { - sealosApp.runEvents('openDesktopApp', { - appKey: 'system-template', - pathname: '/instance', - query: { instanceName: app.source.sourceName } - }); - } - if (app.source.sourceType === 'sealaf') { - sealosApp.runEvents('openDesktopApp', { - appKey: 'system-sealaf', - pathname: '/', - query: { instanceName: app.source.sourceName } - }); - } - }} - > - - {t(app.source?.sourceType)} - - {t('Manage all resources')} - - - - - )} - - <> - - - {t('Application Type')} - - - {appTags.map((tag) => ( - - {t(tag)} - - ))} - - - {appInfoTable.map((info) => ( - - - - {t(info.name)} - - - {info.items.map((item, i) => ( - - - {t(item.label)} - - - - item.value && !!item.copy && copyData(item.copy)} - > - {item.render ? item.render : item.value} - - - - - ))} - - - ))} - - - - {t('Advanced Configuration')} - - - {[ - { label: 'Command', value: app.runCMD || 'Not Configured' }, - { label: 'Parameters', value: app.cmdParam || 'Not Configured' } - ].map((item) => ( - - - {t(item.label)} - - - ))} - {/* env */} - - - - {t('Environment Variables')} - - - - {app.envs?.length > 0 && ( - - {app.envs.map((env, index) => { - const valText = env.value - ? env.value - : env.valueFrom - ? 'value from | ***' - : ''; - return ( - - - {env.key} - - - copyData(valText)} - > - {valText} - - - - ); - })} - - )} - - - - {/* configMap */} - - - - {t('Configuration File')} - - - - 0 - ? { - mb: 3, - border: theme.borders.base - } - : {})} - > - {app.configMapList.map((item) => ( - setDetailConfigMap(item)} - _notLast={{ - borderBottom: theme.borders.base - }} - > - - - {item.mountPath} - - {item.value} - - - - ))} - - - - - {/* store */} - - - - {t('Storage')} - - - - 0 - ? { - mb: 4, - border: theme.borders.base - } - : {})} - > - {app.storeList.map((item) => ( - - - - - {item.path} - - - {item.value} Gi - - - - ))} - - - - - - - - {detailConfigMap && ( - setDetailConfigMap(undefined)} /> - )} - - ); -}; - -export default React.memo(AppBaseInfo); diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx index c9432e8e565e..5842914996b3 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx @@ -8,12 +8,15 @@ import { serviceSideProps } from '@/utils/i18n'; import { Box, Flex, useTheme } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import dynamic from 'next/dynamic'; -import React, { useMemo, useState } from 'react'; -import AppBaseInfo from './components/AppBaseInfo'; -import Header from './components/Header'; -import Pods from './components/Pods'; +import React, { useMemo } from 'react'; +import AppBaseInfo from '@/components/app/detail/index/AppBaseInfo'; +import Pods from '@/components/app/detail/index/Pods'; +import DetailLayout from '@/components/layouts/DetailLayout'; +import AdvancedInfo from '@/components/app/detail/index/AdvancedInfo'; -const AppMainInfo = dynamic(() => import('./components/AppMainInfo'), { ssr: false }); +const AppMainInfo = dynamic(() => import('@/components/app/detail/index/AppMainInfo'), { + ssr: false +}); const AppDetail = ({ appName }: { appName: string }) => { const { startGuide } = useDetailDriver(); @@ -30,9 +33,6 @@ const AppDetail = ({ appName }: { appName: string }) => { loadDetailMonitorData } = useAppStore(); - const [podsLoaded, setPodsLoaded] = useState(false); - const [showSlider, setShowSlider] = useState(false); - const { refetch, isSuccess } = useQuery(['setAppDetail'], () => setAppDetail(appName), { onError(err) { toast({ @@ -42,21 +42,6 @@ const AppDetail = ({ appName }: { appName: string }) => { } }); - useQuery( - ['app-detail-pod'], - () => { - if (appDetail?.isPause) return null; - return intervalLoadPods(appName, true); - }, - { - refetchOnMount: true, - refetchInterval: 3000, - onSettled() { - setPodsLoaded(true); - } - } - ); - useQuery( ['loadDetailMonitorData', appName, appDetail?.isPause], () => { @@ -70,82 +55,31 @@ const AppDetail = ({ appName }: { appName: string }) => { ); return ( - - -
- - - - {appDetail ? : } - - - - {appDetail ? : } + + + + + - - + + {appDetail ? : } + + + + + + - {/* mask */} - {!isLargeScreen && showSlider && ( - setShowSlider(false)} - /> - )} - + ); }; diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/logs.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/logs.tsx new file mode 100644 index 000000000000..853fa7c6344f --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/app/detail/logs.tsx @@ -0,0 +1,263 @@ +import { useTranslation } from 'next-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { Box, useTheme, Flex, Divider } from '@chakra-ui/react'; +import { useAppStore } from '@/store/app'; +import { serviceSideProps } from '@/utils/i18n'; +import DetailLayout from '@/components/layouts/DetailLayout'; +import { Header } from '@/components/app/detail/logs/Header'; +import { Filter } from '@/components/app/detail/logs/Filter'; +import { LogTable } from '@/components/app/detail/logs/LogTable'; +import { LogCounts } from '@/components/app/detail/logs/LogCounts'; +import { useEffect, useMemo, useState } from 'react'; +import { ListItem } from '@/components/AdvancedSelect'; +import useDateTimeStore from '@/store/date'; +import { getAppLogs, getLogPodList } from '@/api/app'; +import { useForm } from 'react-hook-form'; +import { formatTimeRange } from '@/utils/timeRange'; +import { downLoadBold } from '@/utils/tools'; +import { useLogStore } from '@/store/logStore'; +import { useRouter } from 'next/router'; +import { useMessage } from '@sealos/ui'; + +export interface JsonFilterItem { + key: string; + value: string; + mode: '=' | '!=' | '~' | '!~'; +} + +export interface LogsFormData { + pods: ListItem[]; + containers: ListItem[]; + limit: number; + keyword: string; + isJsonMode: boolean; + isOnlyStderr: boolean; + jsonFilters: JsonFilterItem[]; + refreshInterval: number; + filterKeys: { + value: string; + label: string; + }[]; +} + +export default function LogsPage({ appName }: { appName: string }) { + const theme = useTheme(); + const router = useRouter(); + const { message } = useMessage(); + const { t } = useTranslation(); + const { appDetail, appDetailPods } = useAppStore(); + + const { refreshInterval, setRefreshInterval, startDateTime, endDateTime } = useDateTimeStore(); + const { setLogs, exportLogs, parsedLogs, logCounts, setLogCounts } = useLogStore(); + + const formHook = useForm({ + defaultValues: { + pods: [], + containers: [], + limit: 100, + keyword: '', + isJsonMode: false, + isOnlyStderr: false, + jsonFilters: [], + refreshInterval: 0 + } + }); + + const selectedPods = formHook.watch('pods').filter((pod) => pod.checked); + const selectedContainers = formHook.watch('containers').filter((container) => container.checked); + const jsonFilters = formHook + .watch('jsonFilters') + .filter((item) => item.key && item.key.trim() !== ''); + const timeRange = formatTimeRange(startDateTime, endDateTime); + + const { isLoading, refetch: refetchLogsData } = useQuery( + [ + 'logs-data', + appName, + timeRange, + formHook.watch('isOnlyStderr'), + formHook.watch('limit'), + formHook.watch('isJsonMode'), + formHook.watch('keyword'), + selectedPods, + selectedContainers + ], + () => + getAppLogs({ + time: timeRange, + app: appName, + stderrMode: formHook.watch('isOnlyStderr').toString(), + limit: formHook.watch('limit').toString(), + jsonMode: formHook.watch('isJsonMode').toString(), + keyword: formHook.watch('keyword'), + pod: + selectedPods.length === formHook.watch('pods').length + ? [] + : selectedPods.map((pod) => pod.value), + container: + selectedContainers.length === formHook.watch('containers').length + ? [] + : selectedContainers.map((container) => container.value), + jsonQuery: jsonFilters + }), + { + retry: 1, + staleTime: 3000, + cacheTime: 3000, + refetchInterval: refreshInterval, + onError: (error: any) => { + console.log(error, 'error'); + setRefreshInterval(0); + }, + onSuccess: (data) => { + setLogs(data); + } + } + ); + + // log counts + const { refetch: refetchLogCountsData, isLoading: isLogCountsLoading } = useQuery( + [ + 'log-counts-data', + appName, + timeRange, + formHook.watch('isOnlyStderr'), + selectedPods, + selectedContainers, + formHook.watch('isJsonMode'), + formHook.watch('keyword') + ], + () => + getAppLogs({ + app: appName, + numberMode: 'true', + numberLevel: timeRange.slice(-1), + jsonMode: formHook.watch('isJsonMode').toString(), + time: timeRange, + stderrMode: formHook.watch('isOnlyStderr').toString(), + pod: + selectedPods.length === formHook.watch('pods').length + ? [] + : selectedPods.map((pod) => pod.value), + container: + selectedContainers.length === formHook.watch('containers').length + ? [] + : selectedContainers.map((container) => container.value), + jsonQuery: jsonFilters, + keyword: formHook.watch('keyword') + }), + { + refetchInterval: refreshInterval, + staleTime: 3000, + cacheTime: 3000, + onSuccess: (data) => { + setLogCounts(data); + } + } + ); + + const { refetch: refetchPodListData, isLoading: isPodListLoading } = useQuery( + ['log-pod-list-data', appName, timeRange, appDetailPods?.length], + () => + getLogPodList({ + app: appName, + time: timeRange + }), + { + staleTime: 3000, + cacheTime: 3000, + onSuccess: (data) => { + console.log('isInitialized', appDetailPods); + + if (appDetailPods?.length > 0) { + const podList = Array.isArray(data) ? data : []; + const urlPodName = router.query.pod as string; + + const podNamesSet = new Set([ + ...podList, + ...appDetailPods + .map((pod) => pod.metadata?.name) + .filter((name): name is string => !!name) + ]); + + const allPods: ListItem[] = Array.from(podNamesSet).map((podName) => ({ + value: podName, + label: podName, + checked: urlPodName ? podName === urlPodName : true + })); + + formHook.setValue('pods', allPods); + + const containers = appDetailPods + .flatMap((pod) => pod.spec?.containers || []) + .map((container) => ({ + value: container.name, + label: container.name, + checked: true + })) + .filter((item, index, self) => index === self.findIndex((t) => t.value === item.value)); + formHook.setValue('containers', containers); + } + } + } + ); + + const refetchData = () => { + message({ + title: t('refetching_success') + }); + refetchLogsData(); + refetchLogCountsData(); + refetchPodListData(); + }; + + return ( + + + +
+ + + + + + + 0 ? '400px' : '200px'} + > + + + + + ); +} + +export async function getServerSideProps(content: any) { + const appName = content?.query?.name || ''; + + return { + props: { + appName, + ...(await serviceSideProps(content)) + } + }; +} diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/monitor.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/monitor.tsx new file mode 100644 index 000000000000..b58847850ed2 --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/app/detail/monitor.tsx @@ -0,0 +1,234 @@ +import DetailLayout from '@/components/layouts/DetailLayout'; +import { useToast } from '@/hooks/useToast'; +import { useAppStore } from '@/store/app'; +import { serviceSideProps } from '@/utils/i18n'; +import { Box, Center, Skeleton, SkeletonText, Stack, Text } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import Header from '@/components/Monitor/Header'; +import MonitorChart from '@/components/MonitorChart'; +import { useEffect, useMemo, useState } from 'react'; +import { ListItem } from '@/components/AdvancedSelect'; +import useDateTimeStore from '@/store/date'; +import { getAppMonitorData } from '@/api/app'; +import EmptyChart from '@/components/Icon/icons/emptyChart.svg'; + +export default function MonitorPage({ appName }: { appName: string }) { + const { toast } = useToast(); + const { appDetail, appDetailPods } = useAppStore(); + const { t } = useTranslation(); + const { startDateTime, endDateTime } = useDateTimeStore(); + const [podList, setPodList] = useState([]); + const { refreshInterval } = useDateTimeStore(); + + useEffect(() => { + if (appDetailPods?.length > 0 && podList.length === 0) { + setPodList( + appDetailPods.map((pod) => ({ + value: pod.podName, + label: pod.podName, + checked: true + })) + ); + } + }, [appDetailPods, podList]); + + const { + data: memoryData, + isLoading, + refetch: refetchMemoryData + } = useQuery( + ['monitor-data-memory', appName, appDetailPods?.[0]?.podName, startDateTime, endDateTime], + () => + getAppMonitorData({ + queryKey: 'memory', + queryName: appDetailPods?.[0]?.podName || appName, + step: '2m', + start: startDateTime.getTime(), + end: endDateTime.getTime() + }), + { + refetchInterval: refreshInterval, + enabled: !!appDetailPods?.[0]?.podName + } + ); + + const memoryLatestAvg = useMemo(() => { + if (!memoryData?.length) return 0; + + const sum = memoryData.reduce((acc, pod) => { + const lastValue = Number(pod.yData[pod.yData.length - 1]); + return acc + lastValue; + }, 0); + + return (sum / memoryData.length).toFixed(2); + }, [memoryData]); + + const { data: cpuData, refetch: refetchCpuData } = useQuery( + ['monitor-data-cpu', appName, appDetailPods?.[0]?.podName, startDateTime, endDateTime], + () => + getAppMonitorData({ + queryKey: 'cpu', + queryName: appDetailPods?.[0]?.podName || appName, + step: '2m', + start: startDateTime.getTime(), + end: endDateTime.getTime() + }), + { + refetchInterval: refreshInterval, + enabled: !!appDetailPods?.[0]?.podName + } + ); + + const cpuLatestAvg = useMemo(() => { + if (!cpuData?.length) return 0; + + const sum = cpuData.reduce((acc, pod) => { + const lastValue = Number(pod.yData[pod.yData.length - 1]); + return acc + lastValue; + }, 0); + + return (sum / cpuData.length).toFixed(2); + }, [cpuData]); + + const memoryChartData = useMemo(() => { + const selectedPods = podList.filter((pod) => pod.checked); + + const filteredData = memoryData?.filter((item) => + selectedPods.some((pod) => pod.value === item.name) + ); + + if (filteredData?.length === 0) { + return { + xData: [] as string[], + yData: [] as { name: string; type: string; data: number[] }[] + }; + } + + const xData = filteredData?.[0]?.xData.map(String) || []; + const yData = + filteredData?.map((item) => ({ + name: item.name || 'unknown', + type: 'line', + data: item.yData.map(Number) + })) || []; + + return { + xData, + yData + }; + }, [memoryData, podList]); + + const cpuChartData = useMemo(() => { + const selectedPods = podList.filter((pod) => pod.checked); + const filteredData = cpuData?.filter((item) => + selectedPods.some((pod) => pod.value === item.name) + ); + + if (filteredData?.length === 0) { + return { + xData: [] as string[], + yData: [] as { name: string; type: string; data: number[] }[] + }; + } + + const xData = filteredData?.[0]?.xData.map(String) || []; + const yData = + filteredData?.map((item) => ({ + name: item.name || 'unknown', + type: 'line', + data: item.yData.map(Number) + })) || []; + + return { + xData, + yData + }; + }, [cpuData, podList]); + + const refetchData = () => { + refetchCpuData(); + refetchMemoryData(); + }; + + return ( + + +
+ {!isLoading ? ( + <> + + CPU: {cpuLatestAvg}% + + + {cpuChartData?.yData?.length > 0 ? ( + + ) : ( +
+ + + {t('no_data_available')} + +
+ )} +
+ + Memory: {memoryLatestAvg}% + + + {memoryChartData?.yData?.length > 0 ? ( + + ) : ( +
+ + + {t('no_data_available')} + +
+ )} +
+ + ) : ( + + + + + + )} + + + ); +} + +export async function getServerSideProps(content: any) { + const appName = content?.query?.name || ''; + + return { + props: { + appName, + ...(await serviceSideProps(content)) + } + }; +} diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx index c5ede83d5bfe..62a68556299a 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx @@ -94,6 +94,7 @@ const ConfigmapModal = ({ {t('file value')}{' '}