├── .actions └── ASFLicenseHeader.txt ├── .asf.yaml ├── .devcontainer ├── Dockerfile ├── devcontainer.json ├── docker-compose.override.yml └── docker-compose.yml ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE ├── dependabot.yml ├── semantic.yml └── workflows │ ├── codeql-analysis.yml │ ├── e2e.yml │ ├── gitleaks.yml │ └── license-checker.yml ├── .gitignore ├── .gitmodules ├── .husky └── pre-commit ├── .licenserc.yaml ├── .markdownlint.yml ├── .vscode └── settings.json ├── .yamllint ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── docs └── en │ ├── assets │ └── development │ │ ├── command-reopen-in-container.png │ │ ├── env-is-ready.png │ │ └── reopen-in-container.png │ └── development.md ├── e2e ├── pom │ ├── routes.ts │ ├── services.ts │ ├── type.ts │ └── upstreams.ts ├── server │ ├── Dockerfile │ ├── apisix_conf.yml │ ├── docker-compose.common.yml │ ├── docker-compose.yml │ └── nginx.conf ├── tests │ ├── auth.spec.ts │ ├── hot-path.upstream-service-route.spec.ts │ ├── routes.crud-all-fields.spec.ts │ ├── routes.crud-required-fields.spec.ts │ ├── routes.list.spec.ts │ ├── upstreams.crud-all-fields.spec.ts │ ├── upstreams.crud-required-fields.spec.ts │ └── upstreams.list.spec.ts ├── tsconfig.json └── utils │ ├── common.ts │ ├── env.ts │ ├── pagination-test-helper.ts │ ├── req.ts │ ├── test.ts │ └── ui │ ├── index.ts │ └── upstreams.ts ├── eslint.config.ts ├── index.html ├── netlify.toml ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── src ├── apis │ ├── consumer_groups.ts │ ├── consumers.ts │ ├── credentials.ts │ ├── global_rules.ts │ ├── hooks.ts │ ├── plugin_configs.ts │ ├── plugins.ts │ ├── protos.ts │ ├── routes.ts │ ├── secrets.ts │ ├── services.ts │ ├── ssls.ts │ ├── stream_routes.ts │ └── upstreams.ts ├── assets │ └── apisix-logo.svg ├── components │ ├── Btn.tsx │ ├── Header │ │ ├── LanguageMenu.tsx │ │ ├── SettingModalBtn.tsx │ │ └── index.tsx │ ├── Navbar.tsx │ ├── form-slice │ │ ├── FormDisplayDate.tsx │ │ ├── FormItemPlugins │ │ │ ├── PluginCard.tsx │ │ │ ├── PluginCardList.tsx │ │ │ ├── PluginEditorDrawer.tsx │ │ │ ├── SelectPluginsDrawer.tsx │ │ │ └── index.tsx │ │ ├── FormPartBasic.tsx │ │ ├── FormPartConsumer.tsx │ │ ├── FormPartCredential.tsx │ │ ├── FormPartGlobalRules.tsx │ │ ├── FormPartPluginConfig.tsx │ │ ├── FormPartProto.tsx │ │ ├── FormPartRoute │ │ │ ├── index.tsx │ │ │ └── schema.ts │ │ ├── FormPartSSL │ │ │ ├── FormItemCertKeyList.tsx │ │ │ ├── index.tsx │ │ │ └── schema.ts │ │ ├── FormPartSecret.tsx │ │ ├── FormPartService │ │ │ ├── index.tsx │ │ │ └── schema.ts │ │ ├── FormPartStreamRoute │ │ │ ├── index.tsx │ │ │ └── schema.ts │ │ ├── FormPartUpstream │ │ │ ├── FormItemNodes.tsx │ │ │ ├── FormSectionChecks.tsx │ │ │ ├── FormSectionDiscovery.tsx │ │ │ ├── index.tsx │ │ │ ├── schema.ts │ │ │ └── util.ts │ │ ├── FormSection │ │ │ ├── index.tsx │ │ │ └── style.module.css │ │ └── FormSectionGeneral.tsx │ ├── form │ │ ├── Btn.tsx │ │ ├── Editor.tsx │ │ ├── JsonInput.tsx │ │ ├── Labels.tsx │ │ ├── NumberInput.tsx │ │ ├── Select.tsx │ │ ├── Switch.tsx │ │ ├── TagInput.tsx │ │ ├── TextArray.tsx │ │ ├── TextInput.tsx │ │ ├── Textarea.tsx │ │ ├── TextareaWithUpload.tsx │ │ └── util.ts │ ├── page-slice │ │ ├── consumers │ │ │ └── DetailCredentialsTabs.tsx │ │ └── plugin_metadata │ │ │ ├── PluginMetadata.tsx │ │ │ └── hooks.ts │ └── page │ │ ├── DeleteResourceBtn.tsx │ │ ├── PageHeader.tsx │ │ ├── SettingsModal.tsx │ │ ├── Tabs.tsx │ │ └── ToAddPageBtn.tsx ├── config │ ├── antdConfigProvider.tsx │ ├── constant.ts │ ├── global.ts │ ├── i18n.ts │ ├── navRoutes.ts │ └── req.ts ├── locales │ ├── en │ │ └── common.json │ └── zh │ │ └── common.json ├── main.tsx ├── routeTree.gen.ts ├── routes │ ├── __root.tsx │ ├── consumer_groups │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx │ ├── consumers │ │ ├── add.tsx │ │ ├── detail.$username │ │ │ ├── credentials │ │ │ │ ├── add.tsx │ │ │ │ ├── detail.$id.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ ├── global_rules │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx │ ├── index.tsx │ ├── plugin_configs │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx │ ├── plugin_metadata │ │ └── index.tsx │ ├── protos │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx │ ├── routes │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx │ ├── secrets │ │ ├── add.tsx │ │ ├── detail.$manager.$id.tsx │ │ └── index.tsx │ ├── services │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ ├── detail.$id │ │ │ ├── index.tsx │ │ │ └── routes │ │ │ │ └── index.tsx │ │ └── index.tsx │ ├── ssls │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx │ ├── stream_routes │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx │ └── upstreams │ │ ├── add.tsx │ │ ├── detail.$id.tsx │ │ └── index.tsx ├── stores │ └── global.ts ├── styles │ └── global.css ├── types │ ├── i18next.d.ts │ ├── router.d.ts │ ├── schema │ │ ├── apisix │ │ │ ├── common.ts │ │ │ ├── consumer_groups.ts │ │ │ ├── consumers.ts │ │ │ ├── credentials.ts │ │ │ ├── global_rules.ts │ │ │ ├── index.ts │ │ │ ├── plugin_configs.ts │ │ │ ├── plugin_metadata.ts │ │ │ ├── plugins.ts │ │ │ ├── protos.ts │ │ │ ├── routes.ts │ │ │ ├── secrets.ts │ │ │ ├── services.ts │ │ │ ├── ssls.ts │ │ │ ├── stream_routes.ts │ │ │ ├── type.ts │ │ │ └── upstreams.ts │ │ └── pageSearch.ts │ └── vite-env.d.ts └── utils │ ├── form-producer.ts │ ├── producer.ts │ ├── useNamePrefix.ts │ ├── useSearchParams.ts │ ├── useTablePagination.ts │ └── zod.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite-plugin-i18n-progress.ts └── vite.config.ts /.actions/ASFLicenseHeader.txt: -------------------------------------------------------------------------------- 1 | Licensed to the Apache Software Foundation (ASF) under one or more 2 | contributor license agreements. See the NOTICE file distributed with 3 | this work for additional information regarding copyright ownership. 4 | The ASF licenses this file to You under the Apache License, Version 2.0 5 | (the "License"); you may not use this file except in compliance with 6 | the License. You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /.asf.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | github: 19 | description: Dashboard for Apache APISIX 20 | homepage: https://apisix.apache.org/ 21 | labels: 22 | - dashboard 23 | - api 24 | - api-management 25 | - apisix 26 | - devops 27 | 28 | enabled_merge_buttons: 29 | squash: true 30 | merge: false 31 | rebase: false 32 | 33 | protected_branches: 34 | master: 35 | required_pull_request_reviews: 36 | require_code_owner_reviews: true 37 | required_approving_review_count: 2 38 | 39 | notifications: 40 | commits: notifications@apisix.apache.org 41 | issues: notifications@apisix.apache.org 42 | pullrequests: notifications@apisix.apache.org 43 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=22 2 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:${VARIANT} 3 | 4 | # [Optional] Uncomment this section to install additional OS packages. 5 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 6 | # && apt-get -y install --no-install-recommends 7 | 8 | # [Optional] Uncomment if you want to install an additional version of node using nvm 9 | # ARG EXTRA_NODE_VERSION=10 10 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 11 | 12 | # [Optional] Uncomment if you want to install more global node modules 13 | # RUN su node -c "npm install -g " 14 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.base.schema.json", 3 | "name": "APISIX Dashboard Dev Environment", 4 | "dockerComposeFile": [ 5 | "./docker-compose.yml" 6 | ], 7 | "service": "apisix-dashboard", 8 | "workspaceFolder": "/workspace", 9 | "shutdownAction": "stopCompose", 10 | "postCreateCommand": "pnpm i && echo '\nUse `pnpm dev` to continue'", 11 | "forwardPorts": [ 12 | 9080, 13 | 9180, 14 | 9100, 15 | 9200, 16 | 5173, 17 | 5174, 18 | 4173 19 | ], 20 | "portsAttributes": { 21 | "5173": { 22 | "label": "APISIX Dashboard", 23 | "onAutoForward": "ignore" 24 | }, 25 | "5174": { 26 | "label": "APISIX Dashboard HMR WS", 27 | "onAutoForward": "silent" 28 | }, 29 | "9180": { 30 | "label": "APISIX Admin API Port", 31 | "onAutoForward": "notify" 32 | }, 33 | "9100": { 34 | "label": "APISIX Stream Proxy TCP Port", 35 | "onAutoForward": "silent" 36 | }, 37 | "9200": { 38 | "label": "APISIX Stream Proxy UDP Port", 39 | "onAutoForward": "silent" 40 | } 41 | }, 42 | "customizations": { 43 | "vscode": { 44 | "extensions": [ 45 | "vunguyentuan.vscode-css-variables", 46 | "dbaeumer.vscode-eslint", 47 | "drKnoxy.eslint-disable-snippets", 48 | "esbenp.prettier-vscode", 49 | "christian-kohler.path-intellisense", 50 | "lokalise.i18n-ally", 51 | "formulahendry.auto-close-tag", 52 | "formulahendry.auto-rename-tag", 53 | "github.vscode-pull-request-github", 54 | "redhat.vscode-yaml" 55 | ] 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | services: 2 | apisix: 3 | ports: 4 | - '9180:9180/tcp' 5 | - '9080:9080/tcp' 6 | - '9091:9091/tcp' 7 | - '9443:9443/tcp' 8 | etcd: 9 | ports: 10 | - '2379:2379/tcp' 11 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - path: 3 | - ../e2e/server/docker-compose.common.yml 4 | - docker-compose.override.yml 5 | services: 6 | apisix-dashboard: 7 | build: 8 | context: .. 9 | dockerfile: .devcontainer/Dockerfile 10 | command: sleep infinity 11 | volumes: 12 | - ..:/workspace:cached 13 | networks: 14 | - apisix 15 | ports: 16 | - '5173:5173' 17 | - '5174:5174' 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Please use this template for reporting suspected bugs. 3 | labels: [bug] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Issue description 9 | description: A clear and concise description of what the issue is. 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: expected 14 | attributes: 15 | label: Expected behavior 16 | description: A clear and concise description of what you expected to happen. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: howto 21 | attributes: 22 | label: How to Reproduce 23 | description: Describe the how we have to take to reproduce the behavior. 24 | placeholder: | 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: screenshots 33 | attributes: 34 | label: Screenshots 35 | description: Add screenshots to help explain your problem if applicable. 36 | - type: textarea 37 | id: environment 38 | attributes: 39 | label: Environment 40 | value: | 41 | - apisix version (cmd: `apisix version`): 42 | - OS (cmd: `uname -a`): 43 | - OpenResty / Nginx version (cmd: `nginx -V` or `openresty -V`): 44 | - etcd version, if have (cmd: run `etcd --version`): 45 | - apisix-dashboard version, if have: 46 | - Browser version, if have: 47 | - type: textarea 48 | id: additional 49 | attributes: 50 | label: Additional context 51 | description: Do you want to solve this issue? or add any other context about the problem here. 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Visit Apache APISIX 4 | url: https://github.com/apache/apisix 5 | about: Ask questions or discuss with other community members about Apache APISIX 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Create a feature request for the Apache APISIX Dashboard 4 | labels: 'feature' 5 | --- 6 | 7 | # Feature request 8 | 9 | ## Please describe your feature 10 | 11 | A clear and concise description of what you want and what your use case is. 12 | 13 | ## Describe the solution you'd like 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | ## Describe alternatives you've considered 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | ## Additional context 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Please answer these questions before submitting a pull request, **or your PR will get closed**. 2 | 3 | **Why submit this pull request?** 4 | 5 | - [ ] Bugfix 6 | - [ ] New feature provided 7 | - [ ] Improve performance 8 | - [ ] Backport patches 9 | 10 | **What changes will this PR take into?** 11 | 12 | Please update this section with detailed description. 13 | 14 | **Related issues** 15 | 16 | fix/resolve #0001 17 | 18 | **Checklist:** 19 | 20 | - [ ] Did you explain what problem does this PR solve? Or what new features have been added? 21 | - [ ] Have you added corresponding test cases? 22 | - [ ] Have you modified the corresponding document? 23 | - [ ] Is this PR backward compatible? If it is not backward compatible, please discuss on the mailing list first 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "npm" # See documentation for possible values 14 | directory: "/" # Location of package manifests 15 | schedule: 16 | interval: "daily" 17 | 18 | - package-ecosystem: "gomod" # See documentation for possible values 19 | directory: "/" # Location of package manifests 20 | schedule: 21 | interval: "daily" 22 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleOnly: true 2 | types: 3 | - feat 4 | - fix 5 | - docs 6 | - style 7 | - refactor 8 | - perf 9 | - test 10 | - build 11 | - ci 12 | - chore 13 | - revert 14 | - change 15 | - backport 16 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: 17 | - master 18 | paths-ignore: 19 | - 'docs/**' 20 | pull_request: 21 | branches: 22 | - master 23 | paths-ignore: 24 | - 'docs/**' 25 | schedule: 26 | - cron: '18 23 * * 0' 27 | 28 | jobs: 29 | analyze: 30 | name: Analyze 31 | runs-on: ubuntu-latest 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: ['javascript-typescript'] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 38 | # Learn more: 39 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v3 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v3 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 https://git.io/JvXDl 62 | 63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 64 | # and modify them (or add more) to build your code if your project 65 | # uses a compiled language 66 | 67 | # - run: | 68 | # make bootstrap 69 | # make release 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/gitleaks.yml: -------------------------------------------------------------------------------- 1 | # Scan git repos (or files) for secrets using regex and entropy 🔑 2 | 3 | name: gitLeaks 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | paths-ignore: 10 | - 'docs/**' 11 | pull_request: 12 | branches: 13 | - master 14 | paths-ignore: 15 | - 'docs/**' 16 | 17 | jobs: 18 | gitleaks: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: '1' 24 | submodules: recursive 25 | - name: wget 26 | shell: bash 27 | run: | 28 | wget https://raw.githubusercontent.com/ycjcl868/gitleaks/master/.gitleaks.toml 29 | - name: gitleaks-action 30 | uses: ./.github/actions/gitleaks-action 31 | -------------------------------------------------------------------------------- /.github/workflows/license-checker.yml: -------------------------------------------------------------------------------- 1 | name: License checker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | check-license: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Check License Header 19 | uses: apache/skywalking-eyes/header@9f0a5c0571ed1a0c13a16808cd8f59bc22f03883 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | /.pnpm-store 27 | 28 | # Playwright 29 | /test-results/ 30 | /playwright-report/ 31 | /blob-report/ 32 | /playwright/.cache/ 33 | 34 | .eslintcache 35 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".github/actions/gitleaks-action"] 2 | path = .github/actions/gitleaks-action 3 | url = https://github.com/zricethezav/gitleaks-action 4 | [submodule ".github/actions/paths-filter"] 5 | path = .github/actions/paths-filter 6 | url = https://github.com/dorny/paths-filter.git 7 | [submodule ".github/actions/tmate-action"] 8 | path = .github/actions/tmate-action 9 | url = https://github.com/mxschmitt/action-tmate 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | git update-index --again 3 | -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 1 | header: 2 | license: 3 | spdx-id: Apache-2.0 4 | copyright-owner: Apache Software Foundation 5 | 6 | paths-ignore: 7 | - 'dist' 8 | - 'licenses' 9 | - '**/*.md' 10 | - 'LICENSE' 11 | - 'NOTICE' 12 | - '**/*.css' 13 | - '**/*.html' 14 | - '**/*.json' 15 | - '**/*.toml' 16 | - '**/*.yaml' 17 | - 'src/assets/**' 18 | - '.actions/**' 19 | - '.devcontainer/**' 20 | - '.github/**' 21 | - '.husky/**' 22 | - '.vscode/**' 23 | - '.dockerignore' 24 | - '.gitignore' 25 | - '.gitmodules' 26 | - '.yamllint' 27 | - 'eslint.config.js' 28 | - 'src/routeTree.gen.ts' 29 | 30 | comment: on-failure 31 | 32 | dependency: 33 | files: 34 | - package.json 35 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | MD001: false 19 | MD004: false 20 | MD013: false 21 | MD014: false 22 | MD024: false 23 | MD026: false 24 | MD029: false 25 | MD033: false 26 | MD034: false 27 | MD040: false 28 | MD041: false 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "typescriptreact", 4 | "typescript", 5 | ], 6 | "files.readonlyInclude": { 7 | "**/routeTree.gen.ts": true 8 | }, 9 | "files.watcherExclude": { 10 | "**/routeTree.gen.ts": true 11 | }, 12 | "search.exclude": { 13 | "**/routeTree.gen.ts": true 14 | }, 15 | "cssVariables.lookupFiles": [ 16 | "**/*.css", 17 | "**/*.scss", 18 | "**/*.sass", 19 | "**/*.less", 20 | "node_modules/@mantine/core/styles.css" 21 | ], 22 | "i18n-ally.sortKeys": true, 23 | "i18n-ally.localesPaths": [ 24 | "src/locales", 25 | ], 26 | "i18n-ally.keystyle": "nested", 27 | "i18n-ally.enabledFrameworks": [ 28 | "react-i18next" 29 | ], 30 | "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", 31 | "i18n-ally.editor.preferEditor": true, 32 | "i18n-ally.translate.saveAsCandidates": true, 33 | "typescript.tsdk": "node_modules/typescript/lib", 34 | "editor.codeActionsOnSave": { 35 | "source.fixAll.eslint": "always" 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | document-start: false 7 | line-length: false 8 | truthy: false 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache APISIX 2 | Copyright 2019-2023 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apache APISIX Dashboard 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/apache/apisix-dashboard/blob/master/LICENSE) 4 | [![Slack](https://badgen.net/badge/Slack/Join%20Apache%20APISIX?icon=slack)](https://apisix.apache.org/slack) 5 | 6 |

7 | Website • 8 | Docs • 9 | Twitter 10 |

11 | 12 | - The master version should be used with Apache APISIX master version. 13 | - The project will not be released independently but will use a fixed git tag for each APISIX release. 14 | 15 | ## What's Apache APISIX Dashboard 16 | 17 | The Apache APISIX Dashboard is designed to make it as easy as possible for users to operate [Apache APISIX](https://github.com/apache/apisix) through a frontend interface. 18 | 19 | ## Development 20 | 21 | Pull requests are encouraged and always welcome. [Pick an issue](https://github.com/apache/apisix-dashboard/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) and help us out! 22 | 23 | Please refer to the [Development Guide](./docs/en/development.md). 24 | 25 | ## Contributing 26 | 27 | Please refer to the [Contribution Guide](./CONTRIBUTING.md) for a more detailed information. 28 | 29 | ## License 30 | 31 | [Apache License 2.0](./LICENSE) 32 | -------------------------------------------------------------------------------- /docs/en/assets/development/command-reopen-in-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/apisix-dashboard/7055654751c34cd5fe6e1ea4b10d3bd72e1109e1/docs/en/assets/development/command-reopen-in-container.png -------------------------------------------------------------------------------- /docs/en/assets/development/env-is-ready.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/apisix-dashboard/7055654751c34cd5fe6e1ea4b10d3bd72e1109e1/docs/en/assets/development/env-is-ready.png -------------------------------------------------------------------------------- /docs/en/assets/development/reopen-in-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/apisix-dashboard/7055654751c34cd5fe6e1ea4b10d3bd72e1109e1/docs/en/assets/development/reopen-in-container.png -------------------------------------------------------------------------------- /docs/en/development.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Development 3 | --- 4 | 5 | Currently, APISIX Dashboard is a SPA that only supports CSR. For easier development, it is recommended to use `VS Code` and the `Dev Containers` extension. This document uses this approach as an example. 6 | 7 | We also welcome contributions to the documentation, including guides for your preferred development workflows. 8 | 9 | ## Prerequisites 10 | 11 | Please install `VS Code` and follow [Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers) to set up your environment. 12 | 13 | `git`, `node`, `pnpm`, as well as `apisix` and `etcd` are all provided in the `.devcontainer` configuration. 14 | 15 | ## Clone then open the project 16 | 17 | ```sh 18 | $ git clone https://github.com/apache/apisix-dashboard.git 19 | $ cd apisix-dashboard 20 | $ code . 21 | ``` 22 | 23 | ## Start developing 24 | 25 | ### 1. Reopen in Container 26 | 27 | Generally, after opening the project in `VS Code`, a prompt will appear in the bottom right corner. Please click `Reopen in Container`. 28 | 29 | ![Reopen in Container](./assets/development/reopen-in-container.png) 30 | 31 | If there is no prompt, open the `Command Palette`, type `reopen`, and select `Dev Containers: Reopen in Container`. 32 | 33 | ![Click `Dev Containers: Reopen in Container` in `Command Palette`](./assets/development/command-reopen-in-container.png) 34 | 35 | ### 2. Wait for the environment to be ready 36 | 37 | After clicking, it will take some time for the environment to be built, depending on your network conditions. 38 | 39 | Once the environment is ready, similar information will be displayed in the `TERMINAL` tab. 40 | 41 | ![Environment is Ready](./assets/development/env-is-ready.png) 42 | 43 | ### 3. Develop 44 | 45 | Open a new Terminal and execute: 46 | 47 | ```sh 48 | pnpm dev 49 | ``` 50 | 51 | You can then modify the code and preview the updated page in the browser in real-time. 52 | -------------------------------------------------------------------------------- /e2e/pom/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { uiGoto } from '@e2e/utils/ui'; 18 | import { expect, type Page } from '@playwright/test'; 19 | 20 | const locator = { 21 | getRouteNavBtn: (page: Page) => 22 | page.getByRole('link', { name: 'Routes', exact: true }), 23 | getAddRouteBtn: (page: Page) => 24 | page.getByRole('button', { name: 'Add Route', exact: true }), 25 | getAddBtn: (page: Page) => 26 | page.getByRole('button', { name: 'Add', exact: true }), 27 | }; 28 | 29 | const assert = { 30 | isIndexPage: async (page: Page) => { 31 | await expect(page).toHaveURL((url) => url.pathname.endsWith('/routes')); 32 | const title = page.getByRole('heading', { name: 'Routes' }); 33 | await expect(title).toBeVisible(); 34 | }, 35 | isAddPage: async (page: Page) => { 36 | await expect(page).toHaveURL((url) => url.pathname.endsWith('/routes/add')); 37 | const title = page.getByRole('heading', { name: 'Add Route' }); 38 | await expect(title).toBeVisible(); 39 | }, 40 | isDetailPage: async (page: Page) => { 41 | await expect(page).toHaveURL((url) => 42 | url.pathname.includes('/routes/detail') 43 | ); 44 | const title = page.getByRole('heading', { name: 'Route Detail' }); 45 | await expect(title).toBeVisible(); 46 | }, 47 | }; 48 | 49 | const goto = { 50 | toIndex: (page: Page) => uiGoto(page, '/routes'), 51 | toAdd: (page: Page) => uiGoto(page, '/routes/add'), 52 | }; 53 | 54 | export const routesPom = { 55 | ...locator, 56 | ...assert, 57 | ...goto, 58 | }; 59 | -------------------------------------------------------------------------------- /e2e/pom/services.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { uiGoto } from '@e2e/utils/ui'; 18 | import { expect, type Page } from '@playwright/test'; 19 | 20 | const locator = { 21 | getServiceNavBtn: (page: Page) => 22 | page.getByRole('link', { name: 'Services', exact: true }), 23 | getAddServiceBtn: (page: Page) => 24 | page.getByRole('button', { name: 'Add Service', exact: true }), 25 | getAddBtn: (page: Page) => 26 | page.getByRole('button', { name: 'Add', exact: true }), 27 | }; 28 | 29 | const assert = { 30 | isIndexPage: async (page: Page) => { 31 | await expect(page).toHaveURL((url) => url.pathname.endsWith('/services')); 32 | const title = page.getByRole('heading', { name: 'Services' }); 33 | await expect(title).toBeVisible(); 34 | }, 35 | isAddPage: async (page: Page) => { 36 | await expect(page).toHaveURL((url) => 37 | url.pathname.endsWith('/services/add') 38 | ); 39 | const title = page.getByRole('heading', { name: 'Add Service' }); 40 | await expect(title).toBeVisible(); 41 | }, 42 | isDetailPage: async (page: Page) => { 43 | await expect(page).toHaveURL((url) => 44 | url.pathname.includes('/services/detail') 45 | ); 46 | const title = page.getByRole('heading', { name: 'Service Detail' }); 47 | await expect(title).toBeVisible(); 48 | }, 49 | }; 50 | 51 | const goto = { 52 | toIndex: (page: Page) => uiGoto(page, '/services'), 53 | toAdd: (page: Page) => uiGoto(page, '/services/add'), 54 | }; 55 | 56 | export const servicesPom = { 57 | ...locator, 58 | ...assert, 59 | ...goto, 60 | }; 61 | -------------------------------------------------------------------------------- /e2e/pom/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { type Locator, type Page } from '@playwright/test'; 19 | 20 | export type POMLocator = { 21 | getAddBtn: (page: Page) => Locator; 22 | }; 23 | 24 | export type POMAssert = { 25 | isIndexPage: (page: Page) => Promise; 26 | isAddPage: (page: Page) => Promise; 27 | isDetailPage: (page: Page) => Promise; 28 | }; 29 | 30 | export type POMGoto = { 31 | toIndex: (page: Page) => void; 32 | toAdd: (page: Page) => void; 33 | }; 34 | 35 | export type CommonPOM = POMLocator & POMAssert & POMGoto; 36 | -------------------------------------------------------------------------------- /e2e/pom/upstreams.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { uiGoto } from '@e2e/utils/ui'; 18 | import { expect, type Page } from '@playwright/test'; 19 | 20 | const locator = { 21 | getUpstreamNavBtn: (page: Page) => 22 | page.getByRole('link', { name: 'Upstreams' }), 23 | getAddUpstreamBtn: (page: Page) => 24 | page.getByRole('button', { name: 'Add Upstream' }), 25 | getAddBtn: (page: Page) => 26 | page.getByRole('button', { name: 'Add', exact: true }), 27 | }; 28 | 29 | const assert = { 30 | isIndexPage: async (page: Page) => { 31 | await expect(page).toHaveURL((url) => url.pathname.endsWith('/upstreams')); 32 | const title = page.getByRole('heading', { name: 'Upstreams' }); 33 | await expect(title).toBeVisible(); 34 | }, 35 | isAddPage: async (page: Page) => { 36 | await expect(page).toHaveURL((url) => 37 | url.pathname.endsWith('/upstreams/add') 38 | ); 39 | const title = page.getByRole('heading', { name: 'Add Upstream' }); 40 | await expect(title).toBeVisible(); 41 | }, 42 | isDetailPage: async (page: Page) => { 43 | await expect(page).toHaveURL((url) => 44 | url.pathname.includes('/upstreams/detail') 45 | ); 46 | const title = page.getByRole('heading', { name: 'Upstream Detail' }); 47 | await expect(title).toBeVisible(); 48 | }, 49 | }; 50 | 51 | const goto = { 52 | toIndex: (page: Page) => uiGoto(page, '/upstreams'), 53 | toAdd: (page: Page) => uiGoto(page, '/upstreams/add'), 54 | }; 55 | 56 | export const upstreamsPom = { 57 | ...locator, 58 | ...assert, 59 | ...goto, 60 | }; 61 | -------------------------------------------------------------------------------- /e2e/server/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | FROM node:22-alpine AS base 19 | 20 | # install deps 21 | FROM base AS deps 22 | WORKDIR /app 23 | COPY package.json pnpm-lock.yaml* ./ 24 | RUN \ 25 | if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install; \ 26 | else echo 'Lockfile not found.' && exit 1; \ 27 | fi 28 | 29 | # build ui 30 | FROM base AS builder 31 | WORKDIR /app 32 | COPY --from=deps /app/node_modules ./node_modules 33 | COPY . . 34 | # we need git to get commit sha 35 | RUN apk add --no-cache git 36 | RUN corepack enable pnpm && pnpm build 37 | 38 | # serve ui 39 | FROM nginx:1.28.0-alpine-slim AS runner 40 | COPY --from=builder /app/dist /usr/share/nginx/html 41 | -------------------------------------------------------------------------------- /e2e/server/apisix_conf.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | apisix: 19 | node_listen: 9080 # APISIX listening port 20 | enable_ipv6: false 21 | proxy_mode: http&stream 22 | stream_proxy: 23 | tcp: 24 | - 9100 25 | udp: 26 | - 9200 27 | 28 | deployment: 29 | admin: 30 | allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow 31 | - 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test. 32 | 33 | admin_key: 34 | - name: "admin" 35 | key: edd1c9f034335f136f87ad84b625c8f1 36 | role: admin # admin: manage all configuration data 37 | 38 | etcd: 39 | host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster. 40 | - "http://etcd:2379" # multiple etcd address 41 | prefix: "/apisix" # apisix configurations prefix 42 | timeout: 30 # 30 seconds 43 | -------------------------------------------------------------------------------- /e2e/server/docker-compose.common.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | services: 19 | apisix: 20 | image: 'apache/apisix:dev' 21 | restart: always 22 | volumes: 23 | - ./apisix_conf.yml:/usr/local/apisix/conf/config.yaml:ro 24 | depends_on: 25 | - etcd 26 | networks: 27 | - apisix 28 | 29 | etcd: 30 | image: bitnami/etcd:3.5 31 | restart: always 32 | volumes: 33 | - etcd_data:/bitnami/etcd 34 | environment: 35 | ETCD_ENABLE_V2: 'true' 36 | ALLOW_NONE_AUTHENTICATION: 'yes' 37 | ETCD_ADVERTISE_CLIENT_URLS: 'http://etcd:2379' 38 | ETCD_LISTEN_CLIENT_URLS: 'http://0.0.0.0:2379' 39 | networks: 40 | - apisix 41 | 42 | networks: 43 | apisix: 44 | driver: bridge 45 | 46 | volumes: 47 | etcd_data: 48 | -------------------------------------------------------------------------------- /e2e/server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | include: 19 | - docker-compose.common.yml 20 | services: 21 | dashboard-e2e: 22 | build: 23 | context: ../.. 24 | dockerfile: e2e/server/Dockerfile 25 | volumes: 26 | - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro 27 | networks: 28 | - apisix 29 | ports: 30 | - '6174:6174' 31 | depends_on: 32 | - apisix 33 | healthcheck: 34 | test: 35 | [ 36 | 'CMD', 37 | 'wget', 38 | '--quiet', 39 | '--tries=1', 40 | '--spider', 41 | 'http://127.0.0.1:6174/ui/', 42 | ] 43 | interval: 10s 44 | timeout: 5s 45 | retries: 5 46 | start_period: 10s 47 | -------------------------------------------------------------------------------- /e2e/server/nginx.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | server { 19 | listen 6174; 20 | 21 | location /ui { 22 | rewrite ^/ui$ /ui/ permanent; 23 | } 24 | 25 | location /ui/ { 26 | alias /usr/share/nginx/html/; 27 | index index.html; 28 | try_files $uri $uri/ /ui/index.html; 29 | } 30 | 31 | location / { 32 | proxy_pass http://apisix:9180; 33 | proxy_set_header Host $host; 34 | proxy_set_header X-Real-IP $remote_addr; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /e2e/tests/auth.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { getAPISIXConf } from '@e2e/utils/common'; 19 | import { test } from '@e2e/utils/test'; 20 | import { expect } from '@playwright/test'; 21 | 22 | // use empty storage state to avoid auth 23 | test.use({ storageState: { cookies: [], origins: [] } }); 24 | 25 | test('can auth with admin key', { tag: '@auth' }, async ({ page }) => { 26 | const settingsModal = page.getByRole('dialog', { name: 'Settings' }); 27 | const adminKeyInput = page.getByRole('textbox', { name: 'Admin Key' }); 28 | const failedMsg = page.getByText('failed to check token'); 29 | 30 | const checkSettingsModal = async () => { 31 | await expect(failedMsg).toBeVisible(); 32 | await expect(settingsModal).toBeVisible(); 33 | await expect(page.getByText('UI Commit SHA')).toBeVisible(); 34 | }; 35 | 36 | await test.step('fill wrong admin key, settings modal always be visible', async () => { 37 | await checkSettingsModal(); 38 | 39 | await expect(adminKeyInput).toBeEmpty(); 40 | await adminKeyInput.fill('wrong-admin-key'); 41 | await page 42 | .getByRole('dialog', { name: 'Settings' }) 43 | .getByRole('button') 44 | .click(); 45 | 46 | await page.reload(); 47 | await expect(failedMsg).toBeVisible(); 48 | await expect(settingsModal).toBeVisible(); 49 | }); 50 | 51 | await test.step('fill correct admin key, settings modal can be hidden', async () => { 52 | await page.reload(); 53 | await checkSettingsModal(); 54 | 55 | const { adminKey } = await getAPISIXConf(); 56 | await adminKeyInput.clear(); 57 | await adminKeyInput.fill(adminKey); 58 | await page 59 | .getByRole('dialog', { name: 'Settings' }) 60 | .getByRole('button') 61 | .click(); 62 | 63 | await page.reload(); 64 | await expect(failedMsg).toBeHidden(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2021", 5 | "dom" 6 | ], 7 | "allowJs": true, 8 | "sourceMap": false, 9 | "target": "ESNext", 10 | "module": "ESNext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "baseUrl": ".", 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "noImplicitAny": false, 18 | "verbatimModuleSyntax": true, 19 | "paths": { 20 | "@e2e/*": [ 21 | "./*" 22 | ], 23 | "@/*": [ 24 | "../src/*" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "./**/*.ts", 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /e2e/utils/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { access, readFile } from 'node:fs/promises'; 18 | import path from 'node:path'; 19 | 20 | import { nanoid } from 'nanoid'; 21 | import selfsigned from 'selfsigned'; 22 | import { parse } from 'yaml'; 23 | 24 | type APISIXConf = { 25 | deployment: { admin: { admin_key: { key: string }[] } }; 26 | }; 27 | export const getAPISIXConf = async () => { 28 | const currentDir = new URL('.', import.meta.url).pathname; 29 | const confPath = path.join(currentDir, '../server/apisix_conf.yml'); 30 | const file = await readFile(confPath, 'utf-8'); 31 | const res = parse(file) as APISIXConf; 32 | return { adminKey: res.deployment.admin.admin_key[0].key }; 33 | }; 34 | 35 | export const fileExists = async (filePath: string) => { 36 | try { 37 | await access(filePath); 38 | return true; 39 | } catch { 40 | return false; 41 | } 42 | }; 43 | 44 | export const randomId = (info: string) => `${info}_${nanoid()}`; 45 | 46 | export const genTLS = () => { 47 | const { cert, private: key } = selfsigned.generate(); 48 | return { cert, key }; 49 | }; 50 | -------------------------------------------------------------------------------- /e2e/utils/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { config } from 'dotenv'; 18 | import { parseEnv } from 'znv'; 19 | import { z } from 'zod'; 20 | 21 | import { BASE_PATH } from '../../src/config/constant'; 22 | 23 | config({ 24 | path: ['./.env', './.env.local', './.env.development.local'], 25 | }); 26 | 27 | export const env = parseEnv(process.env, { 28 | E2E_TARGET_URL: z 29 | .string() 30 | .url() 31 | .default(`http://localhost:6174${BASE_PATH}/`) 32 | .describe( 33 | `If you want to access the test server from dev container playwright to host e2e server, try http://host.docker.internal:6174${BASE_PATH}/` 34 | ), 35 | }); 36 | -------------------------------------------------------------------------------- /e2e/utils/req.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { type APIRequestContext, request } from '@playwright/test'; 18 | import axios, { type AxiosAdapter } from 'axios'; 19 | import { stringify } from 'qs'; 20 | 21 | import { API_HEADER_KEY, API_PREFIX, BASE_PATH } from '@/config/constant'; 22 | 23 | import { getAPISIXConf } from './common'; 24 | import { env } from './env'; 25 | 26 | export const getPlaywrightRequestAdapter = ( 27 | ctx: APIRequestContext 28 | ): AxiosAdapter => { 29 | return async (config) => { 30 | const { url, data, baseURL } = config; 31 | if (typeof url === 'undefined') { 32 | throw new Error('Need to provide a url'); 33 | } 34 | 35 | type Payload = Parameters[1]; 36 | const payload: Payload = { 37 | headers: config.headers, 38 | method: config.method, 39 | failOnStatusCode: true, 40 | data, 41 | }; 42 | const urlWithBase = `${baseURL}${url}`; 43 | const res = await ctx.fetch(urlWithBase, payload); 44 | 45 | try { 46 | return { 47 | ...res, 48 | data: await res.json(), 49 | config, 50 | status: res.status(), 51 | statusText: res.statusText(), 52 | headers: res.headers(), 53 | }; 54 | } finally { 55 | await res.dispose(); 56 | } 57 | }; 58 | }; 59 | 60 | export const getE2eReq = async (ctx: APIRequestContext) => { 61 | const { adminKey } = await getAPISIXConf(); 62 | const API_URL = env.E2E_TARGET_URL.slice(0, -BASE_PATH.length - 1); 63 | 64 | return axios.create({ 65 | adapter: getPlaywrightRequestAdapter(ctx), 66 | baseURL: `${API_URL}${API_PREFIX}`, 67 | paramsSerializer: (p) => 68 | stringify(p, { 69 | arrayFormat: 'repeat', 70 | }), 71 | headers: { [API_HEADER_KEY]: adminKey }, 72 | }); 73 | }; 74 | 75 | export const e2eReq = await getE2eReq(await request.newContext()); 76 | -------------------------------------------------------------------------------- /e2e/utils/test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { readFile } from 'node:fs/promises'; 18 | import path from 'node:path'; 19 | 20 | import { expect, test as baseTest } from '@playwright/test'; 21 | 22 | import { fileExists, getAPISIXConf } from './common'; 23 | import { env } from './env'; 24 | 25 | export type Test = typeof test; 26 | export const test = baseTest.extend({ 27 | storageState: ({ workerStorageState }, use) => use(workerStorageState), 28 | workerStorageState: [ 29 | async ({ browser }, use) => { 30 | // Use parallelIndex as a unique identifier for each worker. 31 | const id = test.info().parallelIndex; 32 | const fileName = path.resolve( 33 | test.info().project.outputDir, 34 | `.auth/${id}.json` 35 | ); 36 | const { adminKey } = await getAPISIXConf(); 37 | 38 | // file exists and contains admin key, use it 39 | if ( 40 | (await fileExists(fileName)) && 41 | (await readFile(fileName)).toString().includes(adminKey) 42 | ) { 43 | return use(fileName); 44 | } 45 | 46 | const page = await browser.newPage({ storageState: undefined }); 47 | 48 | // have to use env here, because the baseURL is not available in worker 49 | await page.goto(env.E2E_TARGET_URL); 50 | 51 | // we need to authenticate 52 | const settingsModal = page.getByRole('dialog', { name: 'Settings' }); 53 | await expect(settingsModal).toBeVisible(); 54 | const adminKeyInput = page.getByRole('textbox', { name: 'Admin Key' }); 55 | await adminKeyInput.clear(); 56 | await adminKeyInput.fill(adminKey); 57 | await page 58 | .getByRole('dialog', { name: 'Settings' }) 59 | .getByRole('button') 60 | .click(); 61 | 62 | await page.context().storageState({ path: fileName }); 63 | await page.close(); 64 | await use(fileName); 65 | }, 66 | { scope: 'worker' }, 67 | ], 68 | page: async ({ baseURL, page }, use) => { 69 | await page.goto(baseURL); 70 | await use(page); 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /e2e/utils/ui/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { CommonPOM } from '@e2e/pom/type'; 18 | import { type Monaco } from '@monaco-editor/react'; 19 | import { expect, type Locator, type Page } from '@playwright/test'; 20 | 21 | import type { FileRouteTypes } from '@/routeTree.gen'; 22 | 23 | import { env } from '../env'; 24 | 25 | export const uiGoto = (page: Page, path: FileRouteTypes['to']) => { 26 | return page.goto(`${env.E2E_TARGET_URL}${path.substring(1)}`); 27 | }; 28 | 29 | export const uiHasToastMsg = async ( 30 | page: Page, 31 | ...filterOpts: Parameters 32 | ) => { 33 | const alertMsg = page.getByRole('alert').filter(...filterOpts); 34 | await expect(alertMsg).toBeVisible(); 35 | await alertMsg.getByRole('button').click(); 36 | await expect(alertMsg).not.toBeVisible(); 37 | }; 38 | 39 | export async function uiCannotSubmitEmptyForm(page: Page, pom: CommonPOM) { 40 | await pom.getAddBtn(page).click(); 41 | await pom.isAddPage(page); 42 | await uiHasToastMsg(page, { 43 | hasText: 'invalid configuration', 44 | }); 45 | } 46 | 47 | export async function uiFillHTTPStatuses( 48 | input: Locator, 49 | ...statuses: string[] 50 | ) { 51 | for (const status of statuses) { 52 | await input.fill(status); 53 | await input.press('Enter'); 54 | } 55 | } 56 | 57 | export async function uiClearEditor(page: Page) { 58 | await page.evaluate(() => { 59 | (window as unknown as { monaco?: Monaco })?.monaco?.editor 60 | ?.getEditors()[0] 61 | ?.setValue(''); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Apache APISIX Dashboard 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/apisix/*" 3 | to = "http://139.217.190.60/apisix/:splat" 4 | status = 200 5 | 6 | [[redirects]] 7 | from = "/*" 8 | to = "/index.html" 9 | status = 200 10 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { defineConfig, devices } from '@playwright/test'; 18 | 19 | import { env } from './e2e/utils/env'; 20 | 21 | /** 22 | * See https://playwright.dev/docs/test-configuration. 23 | */ 24 | export default defineConfig({ 25 | testDir: './e2e/tests', 26 | outputDir: './test-results', 27 | fullyParallel: true, 28 | forbidOnly: !!process.env.CI, 29 | retries: process.env.CI ? 2 : 0, 30 | workers: process.env.CI ? 1 : undefined, 31 | reporter: [ 32 | ['html'], 33 | ['list'], 34 | [ 35 | '@estruyf/github-actions-reporter', 36 | { 37 | useDetails: true, 38 | showError: true, 39 | }, 40 | ], 41 | ], 42 | use: { 43 | baseURL: env.E2E_TARGET_URL, 44 | trace: 'on-first-retry', 45 | }, 46 | 47 | projects: [ 48 | { 49 | name: 'chromium', 50 | use: { 51 | ...devices['Desktop Chrome'], 52 | viewport: { width: 1920, height: 1080 }, 53 | permissions: ['clipboard-read'], 54 | // use chrome 55 | // channel: "chrome", 56 | }, 57 | }, 58 | ], 59 | }); 60 | -------------------------------------------------------------------------------- /src/apis/consumer_groups.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import { API_CONSUMER_GROUPS } from '@/config/constant'; 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | import type { PageSearchType } from '@/types/schema/pageSearch'; 22 | 23 | export const getConsumerGroupListReq = ( 24 | req: AxiosInstance, 25 | params: PageSearchType 26 | ) => 27 | req 28 | .get(API_CONSUMER_GROUPS, { 29 | params, 30 | }) 31 | .then((v) => v.data); 32 | 33 | export const getConsumerGroupReq = (req: AxiosInstance, id: string) => 34 | req 35 | .get( 36 | `${API_CONSUMER_GROUPS}/${id}` 37 | ) 38 | .then((v) => v.data); 39 | 40 | export const putConsumerGroupReq = ( 41 | req: AxiosInstance, 42 | data: APISIXType['ConsumerGroupPut'] 43 | ) => { 44 | const { id, ...rest } = data; 45 | return req.put< 46 | APISIXType['ConsumerGroupPut'], 47 | APISIXType['RespConsumerGroupDetail'] 48 | >(`${API_CONSUMER_GROUPS}/${id}`, rest); 49 | }; 50 | -------------------------------------------------------------------------------- /src/apis/consumers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import { API_CONSUMERS } from '@/config/constant'; 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | import type { PageSearchType } from '@/types/schema/pageSearch'; 22 | 23 | export const getConsumerListReq = (req: AxiosInstance, params: PageSearchType) => 24 | req 25 | .get(API_CONSUMERS, { 26 | params, 27 | }) 28 | .then((v) => v.data); 29 | 30 | export const getConsumerReq = (req: AxiosInstance, username: string) => 31 | req 32 | .get( 33 | `${API_CONSUMERS}/${username}` 34 | ) 35 | .then((v) => v.data); 36 | 37 | export const putConsumerReq = ( 38 | req: AxiosInstance, 39 | data: APISIXType['ConsumerPut'] 40 | ) => { 41 | return req.put( 42 | API_CONSUMERS, 43 | data 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/apis/credentials.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import type { AxiosInstance } from 'axios'; 19 | 20 | import { API_CREDENTIALS, SKIP_INTERCEPTOR_HEADER } from '@/config/constant'; 21 | import type { APISIXType } from '@/types/schema/apisix'; 22 | import type { APISIXListResponse } from '@/types/schema/apisix/type'; 23 | 24 | export type WithUsername = Pick; 25 | 26 | export const getCredentialListReq = (req: AxiosInstance, params: WithUsername) => 27 | req 28 | .get( 29 | API_CREDENTIALS(params.username), 30 | { 31 | headers: { 32 | [SKIP_INTERCEPTOR_HEADER]: ['404'], 33 | }, 34 | params, 35 | } 36 | ) 37 | .then((v) => v.data) 38 | .catch((e) => { 39 | // 404 means credentials is empty 40 | if (e.response.status === 404) { 41 | const res: APISIXListResponse = { 42 | total: 0, 43 | list: [], 44 | }; 45 | return res; 46 | } 47 | throw e; 48 | }); 49 | 50 | export const getCredentialReq = (req: AxiosInstance, username: string, id: string) => 51 | req 52 | .get( 53 | `${API_CREDENTIALS(username)}/${id}` 54 | ) 55 | .then((v) => v.data); 56 | 57 | export const putCredentialReq = ( 58 | req: AxiosInstance, 59 | data: APISIXType['CredentialPut'] & WithUsername 60 | ) => { 61 | const { username, id, ...rest } = data; 62 | return req.put< 63 | APISIXType['CredentialPut'], 64 | APISIXType['RespCredentialDetail'] 65 | >(`${API_CREDENTIALS(username)}/${id}`, rest); 66 | }; 67 | -------------------------------------------------------------------------------- /src/apis/global_rules.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import type { AxiosInstance } from 'axios'; 19 | 20 | import { API_GLOBAL_RULES } from '@/config/constant'; 21 | import type { APISIXType } from '@/types/schema/apisix'; 22 | 23 | export const getGlobalRuleListReq = (req: AxiosInstance) => 24 | req 25 | .get(API_GLOBAL_RULES) 26 | .then((v) => v.data); 27 | 28 | export const getGlobalRuleReq = (req: AxiosInstance, id: string) => 29 | req 30 | .get( 31 | `${API_GLOBAL_RULES}/${id}` 32 | ) 33 | .then((v) => v.data); 34 | 35 | export const putGlobalRuleReq = ( 36 | req: AxiosInstance, 37 | data: APISIXType['GlobalRulePut'] 38 | ) => { 39 | const { id, ...rest } = data; 40 | return req.put< 41 | APISIXType['GlobalRulePut'], 42 | APISIXType['RespGlobalRuleDetail'] 43 | >(`${API_GLOBAL_RULES}/${id}`, rest); 44 | }; 45 | -------------------------------------------------------------------------------- /src/apis/plugin_configs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import { API_PLUGIN_CONFIGS } from '@/config/constant'; 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | import type { PageSearchType } from '@/types/schema/pageSearch'; 22 | 23 | export const getPluginConfigListReq = (req: AxiosInstance, params: PageSearchType) => 24 | req 25 | .get(API_PLUGIN_CONFIGS, { 26 | params, 27 | }) 28 | .then((v) => v.data); 29 | 30 | export const getPluginConfigReq = (req: AxiosInstance, id: string) => 31 | req 32 | .get( 33 | `${API_PLUGIN_CONFIGS}/${id}` 34 | ) 35 | .then((v) => v.data); 36 | 37 | export const putPluginConfigReq = ( 38 | req: AxiosInstance, 39 | data: APISIXType['PluginConfigPut'] 40 | ) => { 41 | const { id, ...rest } = data; 42 | return req.put< 43 | APISIXType['PluginConfigPut'], 44 | APISIXType['RespPluginConfigDetail'] 45 | >(`${API_PLUGIN_CONFIGS}/${id}`, rest); 46 | }; 47 | -------------------------------------------------------------------------------- /src/apis/protos.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import { API_PROTOS } from '@/config/constant'; 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | import type { PageSearchType } from '@/types/schema/pageSearch'; 22 | 23 | export const getProtoListReq = (req: AxiosInstance, params: PageSearchType) => 24 | req 25 | .get(API_PROTOS, { 26 | params, 27 | }) 28 | .then((v) => v.data); 29 | 30 | export const getProtoReq = (req: AxiosInstance, id: string) => 31 | req 32 | .get(`${API_PROTOS}/${id}`) 33 | .then((v) => v.data); 34 | 35 | export const putProtoReq = (req: AxiosInstance, data: APISIXType['Proto']) => { 36 | const { id, ...rest } = data; 37 | return req.put( 38 | `${API_PROTOS}/${id}`, 39 | rest 40 | ); 41 | }; 42 | 43 | export const postProtoReq = ( 44 | req: AxiosInstance, 45 | data: APISIXType['ProtoPost'] 46 | ) => { 47 | return req.post( 48 | API_PROTOS, 49 | data 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/apis/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import type { RoutePostType } from '@/components/form-slice/FormPartRoute/schema'; 20 | import { API_ROUTES, PAGE_SIZE_MAX, PAGE_SIZE_MIN } from '@/config/constant'; 21 | import type { APISIXType } from '@/types/schema/apisix'; 22 | import type { PageSearchType } from '@/types/schema/pageSearch'; 23 | 24 | export const getRouteListReq = (req: AxiosInstance, params: PageSearchType) => 25 | req 26 | .get(API_ROUTES, { params }) 27 | .then((v) => v.data); 28 | 29 | export const getRouteReq = (req: AxiosInstance, id: string) => 30 | req 31 | .get(`${API_ROUTES}/${id}`) 32 | .then((v) => v.data); 33 | 34 | export const putRouteReq = (req: AxiosInstance, data: APISIXType['Route']) => { 35 | const { id, ...rest } = data; 36 | return req.put( 37 | `${API_ROUTES}/${id}`, 38 | rest 39 | ); 40 | }; 41 | 42 | export const postRouteReq = (req: AxiosInstance, data: RoutePostType) => 43 | req.post(API_ROUTES, data); 44 | 45 | export const deleteAllRoutes = async (req: AxiosInstance) => { 46 | const totalRes = await getRouteListReq(req, { 47 | page: 1, 48 | page_size: PAGE_SIZE_MIN, 49 | }); 50 | const total = totalRes.total; 51 | if (total === 0) return; 52 | for (let times = Math.ceil(total / PAGE_SIZE_MAX); times > 0; times--) { 53 | const res = await getRouteListReq(req, { 54 | page: 1, 55 | page_size: PAGE_SIZE_MAX, 56 | }); 57 | await Promise.all( 58 | res.list.map((d) => req.delete(`${API_ROUTES}/${d.value.id}`)) 59 | ); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/apis/secrets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import { API_SECRETS } from '@/config/constant'; 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | import type { PageSearchType } from '@/types/schema/pageSearch'; 22 | 23 | /** 24 | * `manager` does not exist in apisix secret, we parse it from `id` 25 | * `id` (origin) is `manager/id`, we convert it to `id` and `manager` 26 | */ 27 | export const preParseSecretItem = ( 28 | data: T 29 | ) => { 30 | const { id } = data.value; 31 | if (!id) return data; 32 | type IDTuple = [APISIXType['Secret']['manager'], string]; 33 | const idTuple = id.split('/') as IDTuple; 34 | if (idTuple.length !== 2) return data; 35 | const [manager, realId] = idTuple; 36 | return { ...data, value: { ...data.value, manager, id: realId } }; 37 | }; 38 | 39 | export const getSecretListReq = (req: AxiosInstance, params: PageSearchType) => 40 | req 41 | .get(API_SECRETS, { 42 | params, 43 | }) 44 | .then((v) => { 45 | const { list, ...rest } = v.data; 46 | return { 47 | ...rest, 48 | list: list.map(preParseSecretItem), 49 | }; 50 | }); 51 | 52 | export const getSecretReq = ( 53 | req: AxiosInstance, 54 | props: Pick 55 | ) => { 56 | const { id, manager } = props; 57 | return req 58 | .get( 59 | `${API_SECRETS}/${manager}/${id}` 60 | ) 61 | .then((v) => preParseSecretItem(v.data)); 62 | }; 63 | 64 | export const putSecretReq = ( 65 | req: AxiosInstance, 66 | data: APISIXType['Secret'] 67 | ) => { 68 | const { manager, id, ...rest } = data; 69 | return req.put( 70 | `${API_SECRETS}/${manager}/${id}`, 71 | rest 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/apis/services.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import { API_SERVICES, PAGE_SIZE_MAX, PAGE_SIZE_MIN } from '@/config/constant'; 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | import type { PageSearchType } from '@/types/schema/pageSearch'; 22 | 23 | export type ServicePostType = APISIXType['ServicePost']; 24 | 25 | export const getServiceListReq = (req: AxiosInstance, params: PageSearchType) => 26 | req 27 | .get(API_SERVICES, { 28 | params, 29 | }) 30 | .then((v) => v.data); 31 | 32 | export const getServiceReq = (req: AxiosInstance, id: string) => 33 | req 34 | .get(`${API_SERVICES}/${id}`) 35 | .then((v) => v.data); 36 | 37 | export const putServiceReq = ( 38 | req: AxiosInstance, 39 | data: APISIXType['Service'] 40 | ) => { 41 | const { id, ...rest } = data; 42 | return req.put( 43 | `${API_SERVICES}/${id}`, 44 | rest 45 | ); 46 | }; 47 | 48 | export const postServiceReq = (req: AxiosInstance, data: ServicePostType) => 49 | req.post( 50 | API_SERVICES, 51 | data 52 | ); 53 | 54 | export const deleteAllServices = async (req: AxiosInstance) => { 55 | const totalRes = await getServiceListReq(req, { 56 | page: 1, 57 | page_size: PAGE_SIZE_MIN, 58 | }); 59 | const total = totalRes.total; 60 | if (total === 0) return; 61 | for (let times = Math.ceil(total / PAGE_SIZE_MAX); times > 0; times--) { 62 | const res = await getServiceListReq(req, { 63 | page: 1, 64 | page_size: PAGE_SIZE_MAX, 65 | }); 66 | await Promise.all( 67 | res.list.map((d) => req.delete(`${API_SERVICES}/${d.value.id}`)) 68 | ); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/apis/ssls.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { AxiosInstance } from 'axios'; 18 | 19 | import type { SSLPostType } from '@/components/form-slice/FormPartSSL/schema'; 20 | import { API_SSLS } from '@/config/constant'; 21 | import type { APISIXType } from '@/types/schema/apisix'; 22 | import type { PageSearchType } from '@/types/schema/pageSearch'; 23 | 24 | export const getSSLListReq = (req: AxiosInstance, params: PageSearchType) => 25 | req 26 | .get(API_SSLS, { 27 | params, 28 | }) 29 | .then((v) => v.data); 30 | 31 | export const getSSLReq = (req: AxiosInstance, id: string) => 32 | req 33 | .get(`${API_SSLS}/${id}`) 34 | .then((v) => v.data); 35 | 36 | export const putSSLReq = (req: AxiosInstance, data: APISIXType['SSL']) => { 37 | const { id, ...rest } = data; 38 | return req.put( 39 | `${API_SSLS}/${id}`, 40 | rest 41 | ); 42 | }; 43 | 44 | export const postSSLReq = (req: AxiosInstance, data: SSLPostType) => 45 | req.post(API_SSLS, data); 46 | -------------------------------------------------------------------------------- /src/apis/stream_routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import type { AxiosInstance } from 'axios'; 19 | 20 | import type { StreamRoutePostType } from '@/components/form-slice/FormPartStreamRoute/schema'; 21 | import { API_STREAM_ROUTES } from '@/config/constant'; 22 | import type { APISIXType } from '@/types/schema/apisix'; 23 | import type { PageSearchType } from '@/types/schema/pageSearch'; 24 | 25 | export const getStreamRouteListReq = (req: AxiosInstance, params: PageSearchType) => 26 | req 27 | .get(API_STREAM_ROUTES, { 28 | params, 29 | }) 30 | .then((v) => v.data); 31 | 32 | export const getStreamRouteReq = (req: AxiosInstance, id: string) => 33 | req 34 | .get( 35 | `${API_STREAM_ROUTES}/${id}` 36 | ) 37 | .then((v) => v.data); 38 | 39 | export const putStreamRouteReq = ( 40 | req: AxiosInstance, 41 | data: APISIXType['StreamRoute'] 42 | ) => { 43 | const { id, ...rest } = data; 44 | return req.put< 45 | APISIXType['StreamRoute'], 46 | APISIXType['RespStreamRouteDetail'] 47 | >(`${API_STREAM_ROUTES}/${id}`, rest); 48 | }; 49 | 50 | export const postStreamRouteReq = ( 51 | req: AxiosInstance, 52 | data: StreamRoutePostType 53 | ) => 54 | req.post( 55 | API_STREAM_ROUTES, 56 | data 57 | ); 58 | -------------------------------------------------------------------------------- /src/apis/upstreams.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import type { AxiosInstance } from 'axios'; 19 | 20 | import { API_UPSTREAMS, PAGE_SIZE_MAX, PAGE_SIZE_MIN } from '@/config/constant'; 21 | import type { APISIXType } from '@/types/schema/apisix'; 22 | import type { PageSearchType } from '@/types/schema/pageSearch'; 23 | 24 | export const getUpstreamListReq = ( 25 | req: AxiosInstance, 26 | params: PageSearchType 27 | ) => 28 | req 29 | .get(API_UPSTREAMS, { params }) 30 | .then((v) => v.data); 31 | 32 | export const getUpstreamReq = (req: AxiosInstance, id: string) => 33 | req 34 | .get(`${API_UPSTREAMS}/${id}`) 35 | .then((v) => v.data); 36 | 37 | export const postUpstreamReq = ( 38 | req: AxiosInstance, 39 | data: Partial 40 | ) => 41 | req.post( 42 | API_UPSTREAMS, 43 | data 44 | ); 45 | 46 | export const putUpstreamReq = ( 47 | req: AxiosInstance, 48 | data: APISIXType['Upstream'] 49 | ) => { 50 | const { id, ...rest } = data; 51 | return req.put( 52 | `${API_UPSTREAMS}/${id}`, 53 | rest 54 | ); 55 | }; 56 | 57 | export const deleteAllUpstreams = async (req: AxiosInstance) => { 58 | const totalRes = await getUpstreamListReq(req, { 59 | page: 1, 60 | page_size: PAGE_SIZE_MIN, 61 | }); 62 | const total = totalRes.total; 63 | if (total === 0) return; 64 | for (let times = Math.ceil(total / PAGE_SIZE_MAX); times > 0; times--) { 65 | const res = await getUpstreamListReq(req, { 66 | page: 1, 67 | page_size: PAGE_SIZE_MAX, 68 | }); 69 | await Promise.all( 70 | res.list.map((d) => req.delete(`${API_UPSTREAMS}/${d.value.id}`)) 71 | ); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/assets/apisix-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/Btn.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { Button, type ButtonProps } from '@mantine/core'; 18 | import { createLink } from '@tanstack/react-router'; 19 | import { forwardRef } from 'react'; 20 | 21 | const MantineBtnLinkComponent = forwardRef( 22 | (props, ref) => { 23 | return 72 | )} 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartConsumer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { useFormContext } from 'react-hook-form'; 18 | import { useTranslation } from 'react-i18next'; 19 | 20 | import { FormItemTextInput } from '@/components/form/TextInput'; 21 | import type { APISIXType } from '@/types/schema/apisix'; 22 | 23 | import { FormItemPlugins } from './FormItemPlugins'; 24 | import { FormPartBasic } from './FormPartBasic'; 25 | import { FormSection } from './FormSection'; 26 | 27 | export const FormSectionPluginsOnly = () => { 28 | const { t } = useTranslation(); 29 | return ( 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export const FormPartConsumer = () => { 37 | const { t } = useTranslation(); 38 | const { control } = useFormContext(); 39 | 40 | return ( 41 | <> 42 | 51 | } 52 | /> 53 | 58 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartCredential.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { useTranslation } from 'react-i18next'; 18 | 19 | import { FormItemPlugins } from './FormItemPlugins'; 20 | import { FormPartBasic } from './FormPartBasic'; 21 | import { FormSection } from './FormSection'; 22 | 23 | export const FormPartCredential = () => { 24 | const { t } = useTranslation(); 25 | return ( 26 | <> 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartGlobalRules.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { t } from 'i18next'; 18 | 19 | import { FormItemPlugins } from './FormItemPlugins'; 20 | import { FormSection } from './FormSection'; 21 | 22 | export const FormPartGlobalRules = () => { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartPluginConfig.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { FormPartBasic, type FormPartBasicProps } from './FormPartBasic'; 18 | import { FormSectionPluginsOnly } from './FormPartConsumer'; 19 | 20 | export const FormPartPluginConfig = ( 21 | props: { 22 | basicProps?: FormPartBasicProps; 23 | } = {} 24 | ) => { 25 | const { basicProps } = props; 26 | return ( 27 | <> 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartProto.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { type FieldValues,useFormContext } from 'react-hook-form'; 18 | import { useTranslation } from 'react-i18next'; 19 | 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | 22 | import { 23 | FormItemTextareaWithUpload, 24 | type FormItemTextareaWithUploadProps, 25 | } from '../form/TextareaWithUpload'; 26 | 27 | const fileTypes = '.proto,.pb'; 28 | export const FormPartProto = ( 29 | props: Pick, 'allowUpload'> 30 | ) => { 31 | const { t } = useTranslation(); 32 | const form = useFormContext(); 33 | return ( 34 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartRoute/schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { z } from 'zod'; 18 | 19 | import { APISIX } from '@/types/schema/apisix'; 20 | 21 | export const RoutePostSchema = APISIX.Route.omit({ 22 | id: true, 23 | create_time: true, 24 | update_time: true, 25 | }); 26 | 27 | export type RoutePostType = z.infer; 28 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartSSL/schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { produce } from 'immer'; 18 | import { isNotEmpty } from 'rambdax'; 19 | import { z } from 'zod'; 20 | 21 | import { APISIX, type APISIXType } from '@/types/schema/apisix'; 22 | 23 | const SSLForm = z.object({ 24 | __clientEnabled: z.boolean().optional(), 25 | }); 26 | 27 | export const SSLPostSchema = APISIX.SSL.omit({ 28 | id: true, 29 | create_time: true, 30 | update_time: true, 31 | }).merge(SSLForm); 32 | 33 | export type SSLPostType = z.infer; 34 | 35 | export const SSLPutSchema = APISIX.SSL.merge(SSLForm); 36 | 37 | export type SSLPutType = z.infer; 38 | 39 | export const produceToSSLForm = (data: APISIXType['SSL']) => 40 | produce(data as SSLPutType, (draft) => { 41 | draft.__clientEnabled = isNotEmpty(draft.client); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartService/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { InputWrapper } from '@mantine/core'; 18 | import { useFormContext } from 'react-hook-form'; 19 | import { useTranslation } from 'react-i18next'; 20 | 21 | import { FormItemSwitch } from '@/components/form/Switch'; 22 | import { FormItemTagsInput } from '@/components/form/TagInput'; 23 | import { FormItemTextInput } from '@/components/form/TextInput'; 24 | 25 | import { FormItemPlugins } from '../FormItemPlugins'; 26 | import { FormPartBasic } from '../FormPartBasic'; 27 | import { FormSectionUpstream } from '../FormPartRoute'; 28 | import { FormSection } from '../FormSection'; 29 | import type { ServicePostType } from './schema'; 30 | 31 | 32 | const FormSectionPlugins = () => { 33 | const { t } = useTranslation(); 34 | return ( 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | const FormSectionSettings = () => { 42 | const { t } = useTranslation(); 43 | const { control } = useFormContext(); 44 | return ( 45 | 46 | 47 | 48 | 49 | 54 | 59 | 60 | ); 61 | }; 62 | 63 | export const FormPartService = () => { 64 | return ( 65 | <> 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartService/schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { z } from 'zod'; 18 | 19 | import { APISIXServices } from '@/types/schema/apisix/services'; 20 | 21 | export const ServicePostSchema = APISIXServices.ServicePost; 22 | 23 | export type ServicePostType = z.infer; 24 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartStreamRoute/schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import type { TypeOf } from 'zod'; 18 | 19 | import { APISIXCommon } from '@/types/schema/apisix/common'; 20 | import { APISIXStreamRoutes } from '@/types/schema/apisix/stream_routes'; 21 | 22 | export const StreamRoutePostSchema = APISIXStreamRoutes.StreamRoute.omit({ 23 | create_time: true, 24 | update_time: true, 25 | id: true, 26 | }).merge(APISIXCommon.Basic); 27 | 28 | export type StreamRoutePostType = TypeOf; 29 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartUpstream/FormSectionDiscovery.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { useFormContext } from 'react-hook-form'; 18 | import { useTranslation } from 'react-i18next'; 19 | 20 | import { FormItemJsonInput } from '@/components/form/JsonInput'; 21 | import { useNamePrefix } from '@/utils/useNamePrefix'; 22 | 23 | import { FormItemTextInput } from '../../form/TextInput'; 24 | import { FormSection } from '../FormSection'; 25 | import type { FormPartUpstreamType } from './schema'; 26 | 27 | export const FormSectionDiscovery = () => { 28 | const { t } = useTranslation(); 29 | const { control } = useFormContext(); 30 | const np = useNamePrefix(); 31 | return ( 32 | 33 | 38 | 43 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartUpstream/schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { z } from 'zod'; 18 | 19 | import { APISIX } from '@/types/schema/apisix'; 20 | 21 | // We don't omit id now, as we need it for detail view 22 | export const FormPartUpstreamSchema = APISIX.Upstream.extend({ 23 | __checksEnabled: z.boolean().optional().default(false), 24 | __checksPassiveEnabled: z.boolean().optional().default(false), 25 | }); 26 | 27 | export type FormPartUpstreamType = z.infer; 28 | -------------------------------------------------------------------------------- /src/components/form-slice/FormPartUpstream/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { produce } from 'immer'; 18 | import { isNotEmpty } from 'rambdax'; 19 | 20 | import type { APISIXType } from '@/types/schema/apisix'; 21 | 22 | import type { FormPartUpstreamType } from './schema'; 23 | 24 | export const produceToUpstreamForm = ( 25 | upstream: Partial, 26 | /** default to upstream */ 27 | base: object = upstream 28 | ) => 29 | produce(base, (d: FormPartUpstreamType) => { 30 | d.__checksEnabled = !!upstream.checks && isNotEmpty(upstream.checks); 31 | d.__checksPassiveEnabled = 32 | !!upstream.checks?.passive && isNotEmpty(upstream.checks.passive); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/form-slice/FormSection/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-block: var(--mantine-font-size-md, 0); 3 | 4 | & > fieldset { 5 | margin-bottom: 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/form-slice/FormSectionGeneral.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { Divider } from '@mantine/core'; 18 | import { useFormContext, useWatch } from 'react-hook-form'; 19 | import { useTranslation } from 'react-i18next'; 20 | 21 | import type { APISIXType } from '@/types/schema/apisix'; 22 | 23 | import { FormItemTextInput } from '../form/TextInput'; 24 | import { FormDisplayDate } from './FormDisplayDate'; 25 | import { FormSection } from './FormSection'; 26 | 27 | const DisplayDate = () => { 28 | const { control } = useFormContext(); 29 | const { t } = useTranslation(); 30 | const createTime = useWatch({ control, name: 'create_time' }); 31 | const updateTime = useWatch({ control, name: 'update_time' }); 32 | return ( 33 | <> 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | const FormItemID = () => { 41 | const { control } = useFormContext(); 42 | const { t } = useTranslation(); 43 | return ( 44 | 45 | ); 46 | }; 47 | 48 | export type FormSectionGeneralProps = { 49 | /** will be default to `readOnly` */ 50 | showDate?: boolean; 51 | showID?: boolean; 52 | readOnly?: boolean; 53 | }; 54 | 55 | export const FormSectionGeneral = (props: FormSectionGeneralProps) => { 56 | const { showDate = props.readOnly, showID = true, readOnly = false } = props; 57 | const { t } = useTranslation(); 58 | // we use fieldset disabled to show readonly state 59 | // because mantine readOnly style looks like we can edit 60 | // this is also the way rhf recommends, 61 | // Using disable directly on the component will cause rhf to ignore the value 62 | return ( 63 | 64 | {showID && } 65 | {showID && showDate && } 66 | {showDate && } 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/form/Btn.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import { 18 | Button, 19 | type ButtonProps, 20 | type PolymorphicComponentProps, 21 | } from '@mantine/core'; 22 | import { useFormContext, useFormState } from 'react-hook-form'; 23 | 24 | export const FormSubmitBtn = ( 25 | props: PolymorphicComponentProps<'button', ButtonProps> 26 | ) => { 27 | const form = useFormContext(); 28 | const { isSubmitting } = useFormState(form); 29 | return