├── .commitlintrc.yml
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ ├── enhancement.yml
│ ├── feature-request.yml
│ ├── proposal.yml
│ └── support-request.yml
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── build.yml
│ ├── generate-changelogs.yml
│ ├── pick-build.yml
│ ├── pr-build.yml
│ ├── prerelease.yml
│ ├── publish-docker-image.yml
│ ├── publish-gh-pages.yml
│ ├── release.yml
│ └── sync-upstream.yml
├── .gitignore
├── .gitmodules
├── .gitmodules.d.mk
├── .husky
├── commit-msg
└── pre-commit
├── .lintstagedrc.yml
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOGS.md
├── CODEOWNERS
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── codegen.ts
├── docker-compose.yml
├── docs
├── build.md
├── commit-msg-guide.md
├── getting-started.md
└── preview.webp
├── hack
└── checkout.sh
├── index.html
├── install
├── daed.desktop
├── daed.service
├── friendly-filenames.json
├── icons
│ ├── 1024x1024.png
│ ├── 128x128.png
│ ├── 16x16.png
│ ├── 24x24.png
│ ├── 256x256.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── 512x512.png
│ └── 64x64.png
├── package_after_install.sh
└── package_after_remove.sh
├── package.json
├── pnpm-lock.yaml
├── public
└── logo.webp
├── publish.Dockerfile
├── src
├── App.tsx
├── Router.tsx
├── apis
│ ├── index.ts
│ ├── mutation.ts
│ └── query.ts
├── components
│ ├── ConfigFormModal.tsx
│ ├── ConfigureNodeFormModal
│ │ ├── HTTPForm.tsx
│ │ ├── Hysteria2Form.tsx
│ │ ├── JuicityForm.tsx
│ │ ├── SSForm.tsx
│ │ ├── SSRForm.tsx
│ │ ├── Socks5Form.tsx
│ │ ├── TrojanForm.tsx
│ │ ├── TuicForm.tsx
│ │ ├── V2rayForm.tsx
│ │ └── index.tsx
│ ├── DraggableResourceBadge.tsx
│ ├── DraggableResourceCard.tsx
│ ├── DroppableGroupCard.tsx
│ ├── ExpandedTableRow.tsx
│ ├── FormActions.tsx
│ ├── GroupFormModal.tsx
│ ├── Header.tsx
│ ├── ImportResourceFormModal.tsx
│ ├── PlainTextFormModal.tsx
│ ├── QRCodeModal.tsx
│ ├── RenameFormModal.tsx
│ ├── Section.tsx
│ ├── SelectItemWithDescription.tsx
│ ├── SimpleCard.tsx
│ ├── Table.tsx
│ ├── UpdateSubscriptionAction.tsx
│ └── index.ts
├── constants
│ ├── default.ts
│ ├── editor.ts
│ ├── index.ts
│ ├── misc.ts
│ └── schema.ts
├── contexts
│ └── index.tsx
├── i18n
│ ├── index.ts
│ └── locales
│ │ ├── en.json
│ │ └── zh-Hans.json
├── index.css
├── initialize.ts
├── main.tsx
├── pages
│ ├── Experiment.tsx
│ ├── MainLayout.tsx
│ ├── Orchestrate
│ │ ├── Config.tsx
│ │ ├── DNS.tsx
│ │ ├── Group.tsx
│ │ ├── Node.tsx
│ │ ├── Routing.tsx
│ │ ├── Subscription.tsx
│ │ └── index.tsx
│ ├── Setup.tsx
│ └── index.ts
├── schemas
│ └── gql
│ │ ├── fragment-masking.ts
│ │ ├── gql.ts
│ │ ├── graphql.ts
│ │ └── index.ts
├── store
│ └── index.ts
├── types
│ └── index.d.ts
├── utils
│ ├── dnd-kit.ts
│ ├── helper.ts
│ ├── index.test.ts
│ ├── index.ts
│ └── node.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.commitlintrc.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - '@commitlint/config-conventional'
3 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | Dockerfile
3 | docker-compose.yml
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | #editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/schemas
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | root: true
2 | parser: '@typescript-eslint/parser'
3 | parserOptions:
4 | ecmaFeatures:
5 | jsx: true
6 | ecmaVersion: latest
7 | sourceType: module
8 | env:
9 | node: true
10 | plugins:
11 | - '@typescript-eslint'
12 | - react
13 | - import
14 | extends:
15 | - eslint:recommended
16 | - plugin:@typescript-eslint/eslint-recommended
17 | - plugin:@typescript-eslint/recommended
18 | - plugin:import/recommended
19 | - plugin:prettier/recommended
20 | - plugin:react-hooks/recommended
21 | - plugin:react/recommended
22 | - plugin:react/jsx-runtime
23 | - plugin:@tanstack/eslint-plugin-query/recommended
24 | settings:
25 | import/resolver:
26 | typescript:
27 | node: true
28 | react:
29 | version: detect
30 | rules:
31 | '@typescript-eslint/no-non-null-assertion': off
32 | padding-line-between-statements:
33 | - error
34 | - blankLine: always
35 | prev: '*'
36 | next: return
37 | - blankLine: always
38 | prev: '*'
39 | next: if
40 | - blankLine: always
41 | prev: 'if'
42 | next: '*'
43 | - blankLine: always
44 | prev: '*'
45 | next: switch
46 | - blankLine: always
47 | prev: switch
48 | next: '*'
49 | react/prop-types: off
50 | react/display-name: off
51 | react/self-closing-comp: error
52 | import/newline-after-import: error
53 | import/order:
54 | - error
55 | - newlines-between: always
56 | groups:
57 | - builtin
58 | - type
59 | - external
60 | - internal
61 | - sibling
62 | - index
63 | - parent
64 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: report any bugs
3 | title: '[Bug Report]
'
4 | labels: [topic/bug]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Checks
9 | description: Please search to see if an issue already exists for the bug you encountered.
10 | options:
11 | - label: I have searched the existing issues
12 | required: true
13 | - label: I have read the documentation
14 | required: true
15 | - label: Is it your first time sumbitting an issue
16 | required: true
17 |
18 | - type: textarea
19 | attributes:
20 | label: Current Behavior
21 | description: A concise description of what you're experiencing.
22 | validations:
23 | required: false
24 |
25 | - type: textarea
26 | attributes:
27 | label: Expected Behavior
28 | description: A concise description of what you expected to happen.
29 | validations:
30 | required: false
31 |
32 | - type: textarea
33 | attributes:
34 | label: Steps to Reproduce
35 | description: Steps to reproduce it (as minimally and precisely as possible).
36 | placeholder: |
37 | 1. In this environment...
38 | 2. With this config...
39 | 3. Do ...
40 | 4. See error...
41 | validations:
42 | required: false
43 |
44 | - type: textarea
45 | attributes:
46 | label: Environment
47 | description: |
48 | examples:
49 | - **Daed version**: v0.2.4
50 | - **OS (e.g `cat /etc/os-release`)**: Debian 12
51 | - **Kernel (e.g. `uname -a`)**: 6.4.8-arch1-1
52 | - **Others**: NA
53 | value: |
54 | - **Daed version**:
55 | - **OS (e.g `cat /etc/os-release`)**:
56 | - **Kernel (e.g. `uname -a`)**:
57 | - **Others**:
58 | validations:
59 | required: false
60 |
61 | - type: textarea
62 | attributes:
63 | label: Anything else?
64 | description: |
65 | Config? Links? References? Anything that will give us more context about the issue you are encountering!
66 |
67 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
68 | validations:
69 | required: false
70 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: '📚 Support/Q&A'
4 | url: https://github.com/daeuniverse/daed/discussions/categories/q-a-support
5 | about: >-
6 | Some questions are already answered in our github discussions Q&A section If you don't find an answer, [create a support ticket](https://github.com/daeuniverse/dae/discussions/new?category=q-a-support)
7 |
8 | - name: '💬 dae Documentation Link (en)'
9 | url: https://github.com/daeuniverse/dae/tree/main/docs/en
10 | about: 'daeuniverse Documentation Link'
11 |
12 | - name: '💬 dae Documentation Link (zh)'
13 | url: https://github.com/daeuniverse/dae/tree/main/docs/zh
14 | about: 'daeuniverse Documentation Link'
15 |
16 | - name: '💬 daed Documentation Link'
17 | url: https://github.com/daeuniverse/daed/blob/main/docs/getting-started.md
18 | about: 'daeuniverse Documentation Link'
19 |
20 | - name: '💬 daeuniverse Telegram Support'
21 | url: https://t.me/daeuniverse
22 | about: 'daeuniverse Telegram Support Channel'
23 |
24 | - name: '💬 daeuniverse GitHub Discussion Support'
25 | url: https://github.com/daeuniverse/dae/discussions
26 | about: 'daeuniverse GitHub Discussion Portal'
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement.yml:
--------------------------------------------------------------------------------
1 | name: Enhancement
2 | description: suggest an enhancement to the daed project
3 | title: '[Enhancement] '
4 | labels: [topic/enhancement]
5 | body:
6 | - type: textarea
7 | attributes:
8 | label: Improvement Suggestion
9 | description: What would you like us to improve.
10 | validations:
11 | required: false
12 |
13 | - type: textarea
14 | attributes:
15 | label: Potential Benefits
16 | description: Why is this needed.
17 | validations:
18 | required: false
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: feature request related to daed
3 | title: '[Feature Request] '
4 | labels: ['topic/feature']
5 | body:
6 | - type: textarea
7 | attributes:
8 | label: Greetings
9 | description: general greetings
10 | placeholder: |
11 | Thanks for supporting the @daeuniverse community! If you have a good idea that wishes us to integrate to the current daed project, feel free to elaborate here.
12 |
13 | You may also post your idea on the [Discussions](https://github.com/daeuniverse/daed/discussions) or the daeuniverse Telegram channel (https://t.me/daeuniverse)
14 |
15 | Prior to opening a feature request, please search for existing requests.
16 |
17 | If you find an existing feature that matches your needs, use the 👍 emoji to show your support for it. If the specifics of your use case are not covered in the existing feature request but the idea seems similar enough, please take the time to *add new conversation* which helps the feature's design evolve.
18 |
19 | If you do not find any other existing requests for the feature you desire, you should open a new feature request. Please take the time to help us understand your use-case as precisely as possible. Be sure to demonstrate that you've evaluated existing features and found them unsuitable.
20 | validations:
21 | required: false
22 |
23 | - type: textarea
24 | attributes:
25 | label: Feature Request
26 | description: What feature you would like us to integrate into the daed project.
27 | validations:
28 | required: false
29 |
30 | - type: textarea
31 | attributes:
32 | label: Use Cases
33 | description: Share with us what use cases the proposed new feature is categorized to.
34 | validations:
35 | required: false
36 |
37 | - type: textarea
38 | attributes:
39 | label: Potential Benefits
40 | description: Why is this needed.
41 | validations:
42 | required: false
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/proposal.yml:
--------------------------------------------------------------------------------
1 | name: Proposal
2 | description: new feature proposal (developer only)
3 | title: '[Proposal] '
4 | labels: ['topic/proposal']
5 | body:
6 | - type: textarea
7 | attributes:
8 | label: Proposal
9 | description: What new feature you would like to integrate into the daed project.
10 | validations:
11 | required: false
12 |
13 | - type: textarea
14 | attributes:
15 | label: Use Cases
16 | description: What use cases the proposed new feature is categorized to.
17 | validations:
18 | required: false
19 |
20 | - type: textarea
21 | attributes:
22 | label: Potential Benefits
23 | description: Why is this needed.
24 | validations:
25 | required: false
26 |
27 | - type: textarea
28 | attributes:
29 | label: Scope
30 | description: What needs to be done.
31 | validations:
32 | required: false
33 |
34 | - type: textarea
35 | attributes:
36 | label: Reference
37 | description: Useful links
38 | validations:
39 | required: false
40 |
41 | - type: textarea
42 | attributes:
43 | label: Implementation
44 | description: PR links to this issue.
45 | validations:
46 | required: false
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support-request.yml:
--------------------------------------------------------------------------------
1 | name: Support Request
2 | description: need some help from the community
3 | title: '[Support Request] '
4 | labels: [topic/support]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Checks
9 | description: Please search to see if an issue already exists for the bug you encountered.
10 | options:
11 | - label: I have searched the existing issues
12 | required: true
13 | - label: I have read the documentation
14 | required: true
15 | - label: Is it your first time sumbitting an issue
16 | required: true
17 |
18 | - type: textarea
19 | attributes:
20 | label: Support Request
21 | description: What would you like us to support (In short summary).
22 | validations:
23 | required: false
24 |
25 | - type: textarea
26 | attributes:
27 | label: Current Behavior
28 | description: A concise description of what you're experiencing.
29 | validations:
30 | required: false
31 |
32 | - type: textarea
33 | attributes:
34 | label: Expected Behavior
35 | description: A concise description of what you expected to happen.
36 | validations:
37 | required: false
38 |
39 | - type: textarea
40 | attributes:
41 | label: Steps to Reproduce
42 | description: Steps to reproduce it (as minimally and precisely as possible).
43 | placeholder: |
44 | 1. In this environment...
45 | 2. With this config...
46 | 3. Do ...
47 | 4. See error...
48 | validations:
49 | required: false
50 |
51 | - type: textarea
52 | attributes:
53 | label: Environment
54 | description: |
55 | examples:
56 | - **Daed version**: v0.2.4
57 | - **OS (e.g `cat /etc/os-release`)**: Debian 12
58 | - **Kernel (e.g. `uname -a`)**: 6.4.8-arch1-1
59 | - **Others**: NA
60 | value: |
61 | - **Daed version**:
62 | - **OS (e.g `cat /etc/os-release`)**:
63 | - **Kernel (e.g. `uname -a`)**:
64 | - **Others**:
65 | validations:
66 | required: false
67 |
68 | - type: textarea
69 | attributes:
70 | label: Anything else?
71 | description: |
72 | Config? Links? References? Anything that will give us more context about the issue you are encountering!
73 |
74 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
75 | validations:
76 | required: false
77 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Background
4 |
5 |
6 |
7 | ### Checklist
8 |
9 | - [ ] The Pull Request has been fully tested
10 | - [ ] There's an entry in the CHANGELOGS
11 | - [ ] There is a user-facing docs PR against https://github.com/daeuniverse/daed
12 |
13 | ### Full Changelogs
14 |
15 | - [Implement ...]
16 |
17 | ### Issue Reference
18 |
19 |
20 |
21 | Closes #_[issue number]_
22 |
23 | ### Test Result
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: /
5 | schedule:
6 | interval: daily
7 |
--------------------------------------------------------------------------------
/.github/workflows/generate-changelogs.yml:
--------------------------------------------------------------------------------
1 | name: Generate Changelogs
2 | run-name: 'chore(release): generate changelogs for ${{ inputs.previous_release_tag }}..${{ inputs.future_release_tag }}'
3 |
4 | on:
5 | workflow_dispatch:
6 | inputs:
7 | previous_release_tag:
8 | required: true
9 | description: previous release tag
10 | future_release_tag:
11 | required: true
12 | description: future release tag
13 | dry_run:
14 | required: true
15 | description: dry run
16 | default: true
17 |
18 | jobs:
19 | build:
20 | name: Generate changelogs
21 | runs-on: ubuntu-latest
22 | permissions:
23 | issues: write
24 | steps:
25 | - uses: actions/checkout@v4
26 |
27 | - name: Generate release changelogs
28 | uses: daeuniverse/changelogs-generator-action@main
29 | id: changelog
30 | with:
31 | # https://github.com/daeuniverse/changelogs-generator-action
32 | previousRelease: ${{ inputs.previous_release_tag }}
33 | futureRelease: ${{ inputs.future_release_tag }}
34 | token: ${{ secrets.GH_TOKEN }}
35 |
36 | - name: Print outputs
37 | shell: bash
38 | run: |
39 | echo "${{ steps.changelog.outputs.changelogs }}"
40 |
41 | - name: Create an issue with proposed changelogs
42 | if: ${{ inputs.dry_run == 'false' }}
43 | uses: dacbd/create-issue-action@main
44 | with:
45 | token: ${{ secrets.GH_TOKEN }}
46 | title: '[Release Changelogs] ${{ inputs.future_release_tag }}'
47 | labels: automated-issue,release
48 | assignees: "yqlbu,mzz2017,kunish"
49 | body: |
50 | ${{ steps.changelog.outputs.changelogs }}
51 |
--------------------------------------------------------------------------------
/.github/workflows/publish-docker-image.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker Image
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - "main"
8 | release:
9 | types: [published]
10 |
11 |
12 | jobs:
13 |
14 | build-web:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | with:
19 | submodules: 'recursive'
20 |
21 | - uses: pnpm/action-setup@v3.0.0
22 | with:
23 | version: latest
24 |
25 | - uses: actions/setup-node@v4
26 | with:
27 | cache: pnpm
28 | node-version: latest
29 |
30 | - name: Build
31 | run: |
32 | pnpm install
33 | pnpm build
34 |
35 | - name: Upload artifact - web
36 | uses: actions/upload-artifact@v4
37 | with:
38 | name: web
39 | path: dist
40 |
41 |
42 | publish-docker-image:
43 | runs-on: ubuntu-latest
44 | needs:
45 | - build-web
46 |
47 | steps:
48 | - uses: actions/checkout@v4
49 | with:
50 | submodules: 'recursive'
51 |
52 | - name: Download artifact - web
53 | uses: actions/download-artifact@v4
54 | with:
55 | name: web
56 | path: dist/
57 |
58 | - name: Prepare Tag
59 | # Borrowed from daeuniverse/dae
60 | id: prep
61 | env:
62 | REF: ${{ github.ref }}
63 | run: |
64 | DIR="$(pwd)/dist/"
65 | if [ -d "$DIR" ]; then
66 | ### Take action if $DIR exists ###
67 | echo "Installing config files in ${DIR}..."
68 | else
69 | ### Control will jump here if $DIR does NOT exists ###
70 | echo "Error: ${DIR} not found. Can not continue."
71 | exit 1
72 | fi
73 | if [[ "$REF" == "refs/tags/v"* ]]; then
74 | tag=$(git describe --tags $(git rev-list --tags --max-count=1))
75 | tag=${tag:1}
76 | else
77 | tag=$(git log -1 --format="%cd" --date=short | sed s/-//g)
78 | fi
79 | echo "IMAGE=daeuniverse/daed" >> $GITHUB_OUTPUT
80 | echo "TAG=$tag" >> $GITHUB_OUTPUT
81 |
82 | - name: Set up QEMU
83 | uses: docker/setup-qemu-action@v3
84 |
85 | - name: Set up Docker Buildx
86 | uses: docker/setup-buildx-action@v3
87 | id: buildx
88 |
89 | - name: Login to Docker Hub
90 | uses: docker/login-action@v3
91 | with:
92 | username: ${{ secrets.DOCKERHUB_USERNAME }}
93 | password: ${{ secrets.DOCKERHUB_TOKEN }}
94 |
95 | - name: Login to ghrc.io
96 | uses: docker/login-action@v3
97 | with:
98 | registry: ghcr.io
99 | username: ${{ github.actor }}
100 | password: ${{ secrets.GITHUB_TOKEN }}
101 |
102 | - name: Login to quay.io
103 | uses: docker/login-action@v3
104 | with:
105 | registry: quay.io
106 | username: ${{ github.repository_owner }}
107 | password: ${{ secrets.QUAY_PASS }}
108 |
109 | - name: Build image
110 | if: github.event_name == 'release'
111 | uses: docker/build-push-action@v5
112 | with:
113 | context: .
114 | build-args: DAED_VERSION=${{ steps.prep.outputs.TAG }}
115 | builder: ${{ steps.buildx.outputs.name }}
116 | file: publish.Dockerfile
117 | target: prod
118 | platforms: linux/386,linux/amd64,linux/arm64,linux/arm/v7
119 | push: true
120 | tags: |
121 | ${{ github.repository }}:latest
122 | ${{ github.repository }}:${{ steps.prep.outputs.TAG }}
123 | ghcr.io/${{ github.repository }}:latest
124 | ghcr.io/${{ github.repository }}:${{ steps.prep.outputs.TAG }}
125 | quay.io/${{ github.repository }}:latest
126 | quay.io/${{ github.repository }}:${{ steps.prep.outputs.TAG }}
127 | cache-from: type=gha
128 | cache-to: type=gha,mode=max
129 |
130 | - name: Test Build
131 | if: github.event_name != 'release'
132 | uses: docker/build-push-action@v5
133 | with:
134 | context: .
135 | build-args: DAED_VERSION=${{ steps.prep.outputs.TAG }}
136 | builder: ${{ steps.buildx.outputs.name }}
137 | file: publish.Dockerfile
138 | target: prod
139 | platforms: linux/386,linux/amd64,linux/arm64,linux/arm/v7
140 | push: true
141 | tags: |
142 | ${{ github.repository }}:test
143 | ${{ github.repository }}:${{ steps.prep.outputs.TAG }}-test
144 | ghcr.io/${{ github.repository }}:test
145 | ghcr.io/${{ github.repository }}:${{ steps.prep.outputs.TAG }}-test
146 | quay.io/${{ github.repository }}:test
147 | quay.io/${{ github.repository }}:${{ steps.prep.outputs.TAG }}-test
148 | cache-from: type=gha
149 | cache-to: type=gha,mode=max
150 |
--------------------------------------------------------------------------------
/.github/workflows/publish-gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Publish Github Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | publish-gh-pages:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - uses: pnpm/action-setup@v3.0.0
15 | with:
16 | version: latest
17 |
18 | - uses: actions/setup-node@v4
19 | with:
20 | cache: pnpm
21 | node-version: latest
22 |
23 | - name: build
24 | run: |
25 | pnpm install
26 | pnpm build
27 |
28 | - name: Publish Github Pages
29 | uses: peaceiris/actions-gh-pages@v3
30 | with:
31 | github_token: ${{ secrets.GITHUB_TOKEN }}
32 | publish_dir: ./dist
33 |
--------------------------------------------------------------------------------
/.github/workflows/sync-upstream.yml:
--------------------------------------------------------------------------------
1 | # _ _
2 | # __| | __ _ ___ __| |
3 | # / _` |/ _` |/ _ \/ _` |
4 | # | (_| | (_| | __/ (_| |
5 | # \__,_|\__,_|\___|\__,_|
6 | #
7 | # Copyright (C) 2023 @daeuniverse
8 | #
9 | # This is a open-source software, liscensed under the MIT License.
10 | # See /License for more information.
11 |
12 | name: Synchronize Upstream
13 |
14 | on:
15 | workflow_dispatch:
16 |
17 | jobs:
18 | sync-dae-core:
19 | uses: daeuniverse/ci-seed-jobs/.github/workflows/sync-upstream.yml@master
20 | with:
21 | submodule-name: wing
22 | secrets: inherit
23 |
--------------------------------------------------------------------------------
/.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 | .idea
17 | .DS_Store
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
24 | # Make
25 | daed
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "wing"]
2 | path = wing
3 | url = https://github.com/daeuniverse/dae-wing
4 |
--------------------------------------------------------------------------------
/.gitmodules.d.mk:
--------------------------------------------------------------------------------
1 | submodule_ready=wing/.git
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm commitlint -e
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm test run
5 | pnpm lint-staged
6 |
--------------------------------------------------------------------------------
/.lintstagedrc.yml:
--------------------------------------------------------------------------------
1 | 'package.json': 'sort-package-json'
2 | '*.{js,jsx,ts,tsx}': 'eslint --fix'
3 | '*.{js,jsx,ts,tsx,md,html,css,less,scss,json,yml,yaml,graphql}': 'prettier --write'
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "semi": false,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "graphql.vscode-graphql",
4 | "dbaeumer.vscode-eslint",
5 | "esbenp.prettier-vscode",
6 | "lokalise.i18n-ally"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[css]": {
3 | "editor.defaultFormatter": "esbenp.prettier-vscode"
4 | },
5 | "[graphql]": {
6 | "editor.defaultFormatter": "esbenp.prettier-vscode"
7 | },
8 | "[html]": {
9 | "editor.defaultFormatter": "esbenp.prettier-vscode"
10 | },
11 | "[javascript]": {
12 | "editor.defaultFormatter": "esbenp.prettier-vscode"
13 | },
14 | "[json]": {
15 | "editor.defaultFormatter": "esbenp.prettier-vscode"
16 | },
17 | "[jsonc]": {
18 | "editor.defaultFormatter": "esbenp.prettier-vscode"
19 | },
20 | "[typescript]": {
21 | "editor.defaultFormatter": "esbenp.prettier-vscode"
22 | },
23 | "[typescriptreact]": {
24 | "editor.defaultFormatter": "esbenp.prettier-vscode"
25 | },
26 | "[yaml]": {
27 | "editor.defaultFormatter": "esbenp.prettier-vscode"
28 | },
29 | "editor.codeActionsOnSave": {
30 | "source.fixAll": "explicit",
31 | "source.organizeImports": "explicit"
32 | },
33 | "editor.formatOnSave": true,
34 | "i18n-ally.localesPaths": ["packages/i18n/locales"],
35 | "i18n-ally.keystyle": "nested"
36 | }
37 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @daeuniverse/governance
2 | /src/** @daeuniverse/daed
3 | /docs/** @daeuniverse/docs
4 | CHANGELOGS.md @daeuniverse/release
5 | /.github/** @daeuniverse/infra
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribute
2 |
3 | If you want to contribute to a project and make it better, your help is very welcome. Contributing is also a great way to learn more about social coding on Github, new technologies and and their ecosystems and how to make constructive, helpful bug reports, feature requests and the noblest of all contributions: a good, clean pull request.
4 |
5 | ### Bug Reports and Feature Requests
6 |
7 | If you have found a `bug` or have a `feature request`, please use the search first in case a similar issue already exists. If not, please create an [issue](https://github.com/daeuniverse/dae-wing/issues/new) in this repository
8 |
9 | ### Code
10 |
11 | If you would like to fix a bug or implement a feature, please `fork` the repository and `create a Pull Request`.
12 |
13 | Before you start any Pull Request, `it is recommended that you create an issue` to discuss first if you have any doubts about requirement or implementation. That way you can be sure that the maintainer(s) agree on what to change and how, and you can hopefully get a quick merge afterwards.
14 |
15 | `Pull Requests` can only be merged once all status checks are green.
16 |
17 | ### How to make a clean pull request
18 |
19 | - Create a `personal fork` of the project on Github.
20 | - Clone the fork on your local machine. Your remote repo on Github is called `origin`.
21 | - Add the original repository as a remote called `upstream`.
22 | - If you created your fork a while ago be sure to pull upstream changes into your local repository.
23 | - Create a new branch to work on! Branch from `develop` if it exists, else from `master`.
24 | - Implement/fix your feature, comment your code.
25 | - Follow the code style of the project, including indentation.
26 | - If the project has tests run them!
27 | - Write or adapt tests as needed.
28 | - Add or change the documentation as needed.
29 | - Squash your commits into a single commit with git's [interactive rebase](https://help.github.com/articles/interactive-rebase). Create a new branch if necessary.
30 | - Push your branch to your fork on Github, the remote `origin`.
31 | - From your fork open a pull request in the correct branch. Target the project's `develop` branch if there is one, else go for `master`!
32 | - Once the pull request is approved and merged you can pull the changes from `upstream` to your local repo and delete
33 | your extra branch(es).
34 |
35 | And last but not least: Always write your commit messages in the present tense. Your commit message should describe what the commit, when applied, does to the code – not what you did to the code.
36 |
37 | ### Re-requesting a review
38 |
39 | Please do not ping your reviewer(s) by mentioning them in a new comment. Instead, use the re-request review functionality. Read more about this in the [ GitHub docs, Re-requesting a review ](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request#re-requesting-a-review).
40 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:alpine as build-web
2 |
3 | WORKDIR /build
4 |
5 | COPY . .
6 |
7 | RUN corepack enable
8 | RUN corepack prepare pnpm@latest --activate
9 | RUN pnpm install
10 | RUN pnpm build
11 |
12 |
13 |
14 | FROM golang:1.21-bookworm as build-bundle
15 |
16 | RUN \
17 | apt-get update; apt-get install -y git make llvm-15 clang-15; \
18 | apt-get clean autoclean && apt-get autoremove -y && rm -rf /var/lib/{apt,dpkg,cache,log}/
19 |
20 | # build bundle process
21 | ENV CGO_ENABLED=0
22 | ENV CLANG=clang-15
23 | ARG DAED_VERSION=self-build
24 |
25 | COPY --from=build-web /build/dist /build/web
26 | COPY --from=build-web /build/wing /build/wing
27 |
28 | WORKDIR /build/wing
29 |
30 | RUN make APPNAME=daed VERSION=$DAED_VERSION OUTPUT=daed WEB_DIST=/build/web/ bundle
31 |
32 |
33 |
34 |
35 | FROM alpine
36 |
37 | LABEL org.opencontainers.image.source=https://github.com/daeuniverse/daed
38 |
39 | RUN mkdir -p /usr/local/share/daed/
40 | RUN mkdir -p /etc/daed/
41 | RUN wget -O /usr/local/share/daed/geoip.dat https://github.com/v2rayA/dist-v2ray-rules-dat/raw/master/geoip.dat; \
42 | wget -O /usr/local/share/daed/geosite.dat https://github.com/v2rayA/dist-v2ray-rules-dat/raw/master/geosite.dat
43 | COPY --from=build-bundle /build/wing/daed /usr/local/bin
44 |
45 | EXPOSE 2023
46 |
47 | CMD ["daed", "run", "-c", "/etc/daed"]
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License Copyright (c) 2023 daeuniverse
2 |
3 | Permission is hereby granted, free of
4 | charge, to any person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use, copy, modify, merge,
7 | publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to the
9 | following conditions:
10 |
11 | The above copyright notice and this permission notice
12 | (including the next paragraph) shall be included in all copies or substantial
13 | portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | OUTPUT ?= daed
2 | APPNAME ?= daed
3 | VERSION ?= 0.0.0.unknown
4 |
5 | .PHONY: submodules submodule
6 |
7 | daed:
8 |
9 | all: clean daed
10 |
11 | clean:
12 | rm -rf dist && rm -f daed
13 |
14 | ## Begin Git Submodules
15 | .gitmodules.d.mk: .gitmodules Makefile
16 | @set -e && \
17 | submodules=$$(grep '\[submodule "' .gitmodules | cut -d'"' -f2 | tr '\n' ' ' | tr ' \n' '\n' | sed 's/$$/\/.git/g') && \
18 | echo "submodule_ready=$${submodules}" > $@
19 |
20 | -include .gitmodules.d.mk
21 |
22 | $(submodule_ready): .gitmodules.d.mk
23 | ifdef SKIP_SUBMODULES
24 | @echo "Skipping submodule update"
25 | else
26 | git submodule update --init --recursive -- "$$(dirname $@)" && \
27 | touch $@
28 | endif
29 |
30 | submodule submodules: $(submodule_ready)
31 | @if [ -z "$(submodule_ready)" ]; then \
32 | rm -f .gitmodules.d.mk; \
33 | echo "Failed to generate submodules list. Please try again."; \
34 | exit 1; \
35 | fi
36 | ## End Git Submodules
37 |
38 | ## Begin Web
39 | PFLAGS ?=
40 | ifeq (,$(wildcard ./.git))
41 | PFLAGS += HUSKY=0
42 | endif
43 | dist: package.json pnpm-lock.yaml
44 | $(PFLAGS) pnpm i
45 | pnpm build
46 | ## End Web
47 |
48 | ## Begin Bundle
49 | DAE_WING_READY=wing/graphql/service/config/global/generated_resolver.go
50 |
51 | $(DAE_WING_READY): wing
52 | cd wing && \
53 | $(MAKE) deps && \
54 | cd .. && \
55 | touch $@
56 |
57 | daed: submodule $(DAE_WING_READY) dist
58 | cd wing && \
59 | $(MAKE) OUTPUT=../$(OUTPUT) APPNAME=$(APPNAME) WEB_DIST=../dist VERSION=$(VERSION) bundle
60 | ## End Bundle
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | daed
2 |
3 |
4 |
5 |
6 | A modern web dashboard for dae
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Features
19 |
20 | - [x] Easy to use, with keyboard navigation / shortcuts builtin
21 | - [x] Beautiful and intuitive UI
22 | - [x] Light / Dark mode
23 | - [x] Mobile friendly
24 |
25 | ## Getting Started
26 |
27 | Please refer to [Quick Start Guide](./docs/getting-started.md) to start using `daed` right away!
28 |
29 | ## Contrubuting
30 |
31 | Feel free to open issues or submit your PR, any feedbacks or help are greatly appreciated.
32 |
33 | Special thanks go to all [contributors](https://github.com/daeuniverse/daed/graphs/contributors). If you would like to contribute, please see the [instructions](./CONTRIBUTING.md). Also, it is recommended following the [commit-msg-guide](./docs/commit-msg-guide.md).
34 |
35 | ## License
36 |
37 | Made with passion 🔥 by [@daeuniverse](https://github.com/daeuniverse)
38 |
39 | The project is dual licensed under the [GNU Affero General Public License v3.0 (dae-wing)](https://github.com/daeuniverse/dae-wing/blob/main/LICENSE) and the [MIT License (daed)](https://github.com/daeuniverse/daed/blob/main/LICENSE).
40 |
41 | ### Dependencies used in this project
42 |
43 | - [Graphql](https://graphql.org)
44 | - [React](https://reactjs.org)
45 | - [Mantine](https://mantine.dev)
46 | - [dnd kit](https://dndkit.com)
47 |
--------------------------------------------------------------------------------
/codegen.ts:
--------------------------------------------------------------------------------
1 | import { CodegenConfig } from '@graphql-codegen/cli'
2 |
3 | // eslint-disable-next-line import/no-default-export
4 | export default {
5 | overwrite: true,
6 | schema: process.env.SCHEMA_PATH,
7 | documents: 'src/**/*',
8 | generates: {
9 | 'src/schemas/gql/': {
10 | preset: 'client',
11 | },
12 | },
13 | hooks: { afterOneFileWrite: ['prettier -w'] },
14 | } satisfies CodegenConfig
15 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | daed:
4 | privileged: true
5 | network_mode: host
6 | pid: host
7 | build:
8 | context: .
9 | volumes:
10 | - /sys:/sys
11 | - /etc/daed:/etc/daed
12 |
--------------------------------------------------------------------------------
/docs/build.md:
--------------------------------------------------------------------------------
1 | ## How to run and build this project manually
2 |
3 | ### Prerequisites
4 |
5 | - nodejs
6 | - golang
7 | - clang
8 | - make
9 | - pnpm
10 |
11 | ### Bootstrap
12 |
13 | The following command will bootstrap the stack (`daed`, `dae-wing`, and `dae`) altogether.
14 |
15 | ```bash
16 | make
17 | ```
18 |
19 | ### Advanced use case (Dev ONLY)
20 |
21 | > **Warning**: If you do NOT plan to use custom `Graphql` schema, please ignore this part.
22 |
23 | > **Note**: By default, Graphql type definitions and api bindings are generated automatically on the fly.
24 | > However, if you would like to configure new `schema` for Graphql, use environment variable `SCHEMA_PATH` to specify your schema endpoint
25 | > It can be a `url` starts with http(s) pointing to graphql endpoint or a static graphql schema file
26 | > Optionally, append `-w` or `--watch` at the end of the command to watch upcoming changes
27 |
28 | ```bash
29 | # e.g.
30 | # SCHEMA_PATH=http(s)://example.com/graphql pnpm codegen
31 | # SCHEMA_PATH=http(s)://example.com/graphql.schema pnpm codegen
32 |
33 | SCHEMA_PATH=/path/to/SCHEMA_PATH pnpm codegen --watch
34 | ```
35 |
36 | ### Spin up server locally
37 |
38 | ```bash
39 | sudo chmod +x ./daed
40 | sudo install -Dm755 daed /usr/bin/
41 | sudo daed run
42 |
43 | # helper
44 | sudo daed [-h,--help]
45 | ```
46 |
47 | If everything goes well, open your browser and navigate to `http://localhost:2023`
48 |
49 | Happy Hacking!
50 |
--------------------------------------------------------------------------------
/docs/commit-msg-guide.md:
--------------------------------------------------------------------------------
1 | # Semantic Commit Messages
2 |
3 | ## The reasons for these conventions
4 |
5 | - automatic generating of the changelog
6 | - simple navigation through Git history (e.g. ignoring the style changes)
7 |
8 | See how a minor change to your commit message style can make you a better developer.
9 |
10 | ## Format
11 |
12 | ```
13 | `(): `
14 |
15 | `` is optional
16 | ```
17 |
18 | ## Example
19 |
20 | ```
21 | feat: add hat wobble
22 | ^--^ ^------------^
23 | | |
24 | | +-> Summary in present tense.
25 | |
26 | +-------> Type: chore, docs, feat, fix, refactor, style, or test.
27 | ```
28 |
29 | Example `` values:
30 |
31 | - `feat`: (new feature for the user, not a new feature for build script)
32 | - `fix`: (bug fix for the user, not a fix to a build script)
33 | - `docs`: (changes to the documentation)
34 | - `style`: (formatting, missing semi colons, etc; no production code change)
35 | - `refactor`: (refactoring production code, eg. renaming a variable)
36 | - `test`: (adding missing tests, refactoring tests; no production code change)
37 | - `chore`: (updating grunt tasks etc; no production code change, e.g. dependencies upgrade)
38 | - `perf`: (perfomance improvement change, e.g. better concurrency performance)
39 | - `ci`: (updating CI configuration files and scripts e.g. `.gitHub/workflows/*.yml` )
40 |
41 | Example `` values:
42 |
43 | - `init`
44 | - `runner`
45 | - `watcher`
46 | - `config`
47 | - `web-server`
48 | - `proxy`
49 |
50 | The `` can be empty (e.g. if the change is a global or difficult to assign to a single component), in which case the parentheses are omitted. In smaller projects such as Karma plugins, the `` is empty.
51 |
52 | ## Message Subject (First Line)
53 |
54 | The first line cannot be longer than `72` characters and should be followed by a blank line. The type and scope should always be lowercase as shown below
55 |
56 | ## Message Body
57 |
58 | use as in the ``, use the imperative, present tense: "change" not "changed" nor "changes". Message body should include motivation for the change and contrasts with previous behavior.
59 |
60 | ## Message footer
61 |
62 | ### Referencing issues
63 |
64 | Closed issues should be listed on a separate line in the footer prefixed with "Closes" keyword as the following:
65 |
66 | ```
67 | Closes #234
68 | ```
69 |
70 | or in the case of multiple issues:
71 |
72 | ```
73 | Closes #123, #245, #992
74 | ```
75 |
76 | ## References
77 |
78 | -
79 | -
80 | -
81 | -
82 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Quick Start Guide
2 |
3 | > **Note**
4 | > `daed` (UI component) is bundled with [dae-wing](https://github.com/daeuniverse/dae-wing) (backend API server) and [dae](https://github.com/daeuniverse/dae) (core).
5 |
6 | ## How to run
7 |
8 | > **Note**
9 | > - NEVER LET YOUR COMPUTER SLEEP OR HIBIRNATE WHILE PROXY IS STILL ON!
10 | > - NEVER SWITCH NETWORK WHILE PROXY IS STILL ON!
11 | > - TURN IT OFF BEFORE YOU MOVE OR LEAVE YOUR COMPUTER!
12 | > - OR YOU WILL HAVE POSSIBILITY TO NEED A REBOOT TO RECOVER YOUR NETWORK CONNECTIVITY!
13 |
14 | ### Download pre-compiled binaries
15 |
16 | Releases are available in
17 |
18 | > **Note**
19 | > If you would like to get a taste of new features, there are `PR Builds` available. Most of the time, newly proposed changes will be included in `PRs` and will be exported as cross-platform executable binaries in builds (GitHub Action Workflow Build). Noted that newly introduced features are sometimes buggy, do it at your own risk. However, we still highly encourage you to check out our latest builds as it may help us further analyze features stability and resolve potential bugs accordingly.
20 |
21 | PR-builds are available in
22 |
23 | ### Spin up server locally
24 |
25 | ```bash
26 | sudo chmod +x ./daed
27 | sudo install -Dm755 daed /usr/bin/
28 | sudo daed run
29 |
30 | # helper
31 | sudo daed [-h,--help]
32 | ```
33 |
34 | ### Debian / Ubuntu
35 |
36 | Releases are available in or the following command gets the latest version of the precompiled installation package consistent with your current system architecture
37 |
38 | ``````shell
39 | # Download
40 | wget -P /tmp https://github.com/daeuniverse/daed/releases/latest/download/installer-daed-linux-$(arch).deb
41 |
42 | # install
43 | sudo dpkg -i /tmp/installer-daed-linux-$(arch).deb
44 | rm /tmp/installer-daed-linux-$(arch).deb
45 |
46 | # Start daed
47 | sudo systemctl start daed
48 |
49 | # enable daed start automatically
50 | sudo systemctl enable daed
51 | ``````
52 |
53 | ### Red Hat / Fedora
54 |
55 | #### Fedora Copr
56 |
57 | daed has been released on [Fedora Copr](https://copr.fedorainfracloud.org/coprs/zhullyb/v2rayA/package/daed).
58 |
59 | ```shell
60 | sudo dnf copr enable zhullyb/v2rayA
61 | sudo dnf install daed
62 | ```
63 |
64 | #### RPM Installation
65 |
66 | Releases are available in or the following command gets the latest version of the precompiled installation package consistent with your current system architecture
67 |
68 | ``````shell
69 | # Download
70 | wget -P /tmp https://github.com/daeuniverse/daed/releases/latest/download/installer-daed-linux-$(arch).rpm
71 |
72 | # install
73 | sudo rpm -ivh /tmp/installer-daed-linux-$(arch).rpm
74 | rm /tmp/installer-daed-linux-$(arch).rpm
75 |
76 | # Start daed
77 | sudo systemctl start daed
78 |
79 | # enable daed start automatically
80 | sudo systemctl enable daed
81 | ``````
82 |
83 | ### openSUSE
84 |
85 | Releases are available in or the following command gets the latest version of the precompiled installation package consistent with your current system architecture
86 |
87 | ``````shell
88 | # Download
89 | wget -P /tmp https://github.com/daeuniverse/daed/releases/latest/download/installer-daed-linux-$(arch).rpm
90 |
91 | # install
92 | sudo zypper install /tmp/installer-daed-linux-$(arch).rpm
93 | rm /tmp/installer-daed-linux-$(arch).rpm
94 |
95 | # Start daed
96 | sudo systemctl start daed
97 |
98 | # enable daed start automatically
99 | sudo systemctl enable daed
100 | ``````
101 |
102 | ### Arch Linux
103 |
104 | Releases are available in or the following command installs the latest version of the precompiled installation package consistent with your current system architecture
105 |
106 | #### AUR
107 |
108 | ##### Latest Release (Optimized Binary for x86-64 v3 / AVX2)
109 |
110 | ``````shell
111 | [yay/paru] -S daed-avx2-bin
112 | ``````
113 |
114 | ##### Latest Release (General x86-64 or aarch64)
115 |
116 | ``````shell
117 | [yay/paru] -S daed
118 | ``````
119 |
120 | ##### Latest Git Version
121 |
122 | ``````shell
123 | [yay/paru] -S daed-git
124 | ``````
125 |
126 | #### archlinuxcn
127 |
128 | ##### Latest Release (Optimized Binary for x86-64 v3 / AVX2)
129 |
130 | ``````shell
131 | sudo pacman -S daed-avx2-bin
132 | ``````
133 |
134 | ##### Latest Release (General x86-64 or aarch64)
135 |
136 | ``````shell
137 | sudo pacman -S daed
138 | ``````
139 |
140 | ##### Latest Git Version
141 |
142 | ``````shell
143 | sudo pacman -S daed-git
144 | ``````
145 |
146 | ### Docker (Experimental)
147 |
148 | Pre-built Docker images are available in `ghcr.io/daeuniverse/daed`, `quay.io/daeuniverse/daed` and `daeuniverse/daed`.
149 |
150 | #### Take `ghcr.io` for example, the command below pulls and runs the latest image
151 |
152 | ```shell
153 | sudo docker run -d \
154 | --privileged \
155 | --network=host \
156 | --pid=host \
157 | --restart=unless-stopped \
158 | -v /sys:/sys \
159 | -v /etc/daed:/etc/daed \
160 | --name=daed \
161 | ghcr.io/daeuniverse/daed:latest
162 | ```
163 |
164 | #### You may also build from source:
165 |
166 | ```shell
167 | # clone the repository
168 | git clone https://github.com/daeuniverse/daed --recursive
169 |
170 | # build the image
171 | docker build -t daed .
172 |
173 | # run the container
174 | sudo docker run -d \
175 | --privileged \
176 | --network=host \
177 | --pid=host \
178 | --restart=unless-stopped \
179 | -v /sys:/sys \
180 | -v /etc/daed:/etc/daed \
181 | --name=daed \
182 | daed
183 | ```
184 |
185 |
186 | > **NOTE**
187 | > - Docker currently supports only i386(x86-32), amd64(x86-64), armv7 and arm64(armv8). (Alpha)
188 | > - Only amd64 is tested working as expected, but not fully tested yet. (Beta)
189 | > - For self build from source, only amd64 and arm64 will run.
190 | > - Please refer to https://github.com/daeuniverse/daed/discussions/291 and relevant PRs and Issues for details
191 | > - Volunteers are welcomed.
192 |
193 |
194 | ## Access Panel
195 |
196 | If everything goes well, open your browser and navigate to `http://localhost:2023`
197 |
198 | Happy Hacking!
199 |
--------------------------------------------------------------------------------
/docs/preview.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/docs/preview.webp
--------------------------------------------------------------------------------
/hack/checkout.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ask_continue() {
4 | read -r -p "All your code changes will be lost, continue? [y/N] " response
5 | case "$response" in
6 | [yY][eE][sS]|[yY])
7 | echo $response
8 | ;;
9 | *)
10 | exit 0
11 | ;;
12 | esac
13 | }
14 |
15 | repo_rinse() {
16 | git reset --hard
17 | git pull
18 | git submodule foreach --recursive git reset --hard
19 | git submodule update --init --recursive
20 | }
21 |
22 | checkout_wing() {
23 | branch="$1"
24 | ask_continue
25 | repo_rinse
26 | cd wing && git fetch
27 | result=$(git branch -l -r "*/$branch")
28 | if [[ "$result" == "" ]]; then
29 | echo "No such branch: $branch"
30 | exit 1
31 | fi
32 | git checkout "$branch"
33 | git pull
34 | git submodule update --recursive
35 | go mod tidy
36 | }
37 |
38 | checkout_core() {
39 | branch="$1"
40 | ask_continue
41 | repo_rinse
42 | cd wing/dae-core && git fetch
43 | result=$(git branch -l -r "*/$branch")
44 | if [[ "$result" == "" ]]; then
45 | echo "No such branch: $branch"
46 | exit 1
47 | fi
48 | git checkout "$branch"
49 | git pull
50 | git submodule update --recursive
51 | cd ..
52 | go mod tidy
53 | }
54 |
55 | show_helps() {
56 | echo -e "\033[1;4mUsage:\033[0m"
57 | echo " $0 [command]"
58 | echo ' '
59 | echo -e "\033[1;4mAvailable commands:\033[0m"
60 | echo " core checkout dae-core to given branch"
61 | echo " wing checkout dae-wing to given branch"
62 | echo " help show this help message"
63 | }
64 |
65 | # Main
66 | set -e
67 | current_dir=$(pwd)
68 | while [ $# != 0 ] ; do
69 | case "$1" in
70 | core)
71 | opt_core=1
72 | shift
73 | break
74 | ;;
75 | dae-core)
76 | opt_core=1
77 | shift
78 | break
79 | ;;
80 | wing)
81 | opt_wing=1
82 | shift
83 | break
84 | ;;
85 | dae-wing)
86 | opt_wing=1
87 | shift
88 | break
89 | ;;
90 | -h)
91 | opt_help=1
92 | shift
93 | break
94 | ;;
95 | *)
96 | opt_help=1
97 | echo "${RED}error: Unknown command: $1${RESET}"
98 | shift
99 | break
100 | ;;
101 | esac
102 | done
103 | if [ ! -z "$opt_help" ];then
104 | show_helps
105 | exit 1
106 | fi
107 |
108 | if [ ! -z "$opt_core" ];then
109 | checkout_core $1
110 | exit 0
111 | fi
112 |
113 | if [ ! -z "$opt_wing" ];then
114 | checkout_wing $1
115 | exit 0
116 | fi
117 |
118 | trap 'cd "$current_dir"' 0 1 2 3
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 | daed
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/install/daed.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Version=1.0
3 | Terminal=false
4 | Type=Application
5 | Name=daed Web Panel
6 | GenericName=A modern web dashboard for dae
7 | GenericName[zh_CN]=一款时尚的 dae 网络仪表盘
8 | Comment=A high-performance proxy tool based on eBPF
9 | Comment[zh_CN]=一款基于eBPF的高性能代理工具
10 | Categories=Network;
11 | Keywords=Internet;VPN;Proxy;
12 | Exec=xdg-open "http://127.0.0.1:2023"
13 | Icon=daed
14 |
15 |
--------------------------------------------------------------------------------
/install/daed.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=daed is a integration solution of dae, API and UI.
3 | Documentation=https://github.com/daeuniverse/daed
4 | After=network-online.target docker.service systemd-sysctl.service
5 | Wants=network-online.target
6 | Conflicts=dae.service
7 |
8 | [Service]
9 | Type=simple
10 | User=root
11 | LimitNPROC=512
12 | LimitNOFILE=1048576
13 | ExecStart=/usr/bin/daed run -c /etc/daed/
14 | Restart=on-abnormal
15 |
16 | [Install]
17 | WantedBy=multi-user.target
18 |
--------------------------------------------------------------------------------
/install/friendly-filenames.json:
--------------------------------------------------------------------------------
1 | {
2 | "linux-386": { "friendlyName": "linux-x86_32" },
3 | "linux-amd64v1": { "friendlyName": "linux-x86_64" },
4 | "linux-amd64v2": { "friendlyName": "linux-x86_64_v2_sse" },
5 | "linux-amd64v3": { "friendlyName": "linux-x86_64_v3_avx2" },
6 | "linux-amd64": { "friendlyName": "linux-x86_64" },
7 | "linux-arm5": { "friendlyName": "linux-armv5" },
8 | "linux-arm6": { "friendlyName": "linux-armv6" },
9 | "linux-arm7": { "friendlyName": "linux-armv7" },
10 | "linux-arm64": { "friendlyName": "linux-arm64" },
11 | "linux-mips64le": { "friendlyName": "linux-mips64le" },
12 | "linux-mips64": { "friendlyName": "linux-mips64" },
13 | "linux-mipsle": { "friendlyName": "linux-mips32le" },
14 | "linux-mips": { "friendlyName": "linux-mips32" },
15 | "linux-riscv64": { "friendlyName": "linux-riscv64" }
16 | }
17 |
--------------------------------------------------------------------------------
/install/icons/1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/1024x1024.png
--------------------------------------------------------------------------------
/install/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/128x128.png
--------------------------------------------------------------------------------
/install/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/16x16.png
--------------------------------------------------------------------------------
/install/icons/24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/24x24.png
--------------------------------------------------------------------------------
/install/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/256x256.png
--------------------------------------------------------------------------------
/install/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/32x32.png
--------------------------------------------------------------------------------
/install/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/48x48.png
--------------------------------------------------------------------------------
/install/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/512x512.png
--------------------------------------------------------------------------------
/install/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/install/icons/64x64.png
--------------------------------------------------------------------------------
/install/package_after_install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | systemctl daemon-reload
3 |
4 | if [ "$(systemctl is-active daed)" == 'active' ]; then
5 | systemctl restart daed.service
6 | echo "Restarting daed service, it might take a while."
7 | fi
--------------------------------------------------------------------------------
/install/package_after_remove.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | systemctl daemon-reload
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "daed",
3 | "version": "v0.1.0",
4 | "private": true,
5 | "description": "A Web Dashboard For dae",
6 | "homepage": "https://daeuniverse.github.io/daed",
7 | "repository": "https://github.com/daeuniverse/daed",
8 | "license": "MIT",
9 | "author": {
10 | "name": "daeuniverse",
11 | "email": "dae@v2raya.org"
12 | },
13 | "scripts": {
14 | "build": "vite build",
15 | "codegen": "graphql-codegen",
16 | "dev": "vite dev",
17 | "lint": "vite lint",
18 | "prepare": "husky install",
19 | "test": "vitest"
20 | },
21 | "dependencies": {
22 | "@commitlint/cli": "^18.6.1",
23 | "@commitlint/config-conventional": "^18.6.3",
24 | "@dnd-kit/core": "^6.1.0",
25 | "@dnd-kit/modifiers": "^6.0.1",
26 | "@dnd-kit/sortable": "^7.0.2",
27 | "@dnd-kit/utilities": "^3.2.2",
28 | "@emotion/react": "^11.13.3",
29 | "@faker-js/faker": "^8.4.1",
30 | "@fontsource/fira-sans": "^5.1.0",
31 | "@fontsource/source-code-pro": "^5.1.0",
32 | "@graphiql/toolkit": "^0.9.2",
33 | "@graphql-codegen/cli": "5.0.0",
34 | "@graphql-codegen/client-preset": "4.1.0",
35 | "@graphql-codegen/introspection": "4.0.0",
36 | "@graphql-typed-document-node/core": "^3.2.0",
37 | "@mantine/carousel": "^6.0.22",
38 | "@mantine/core": "^6.0.22",
39 | "@mantine/dates": "^6.0.22",
40 | "@mantine/dropzone": "^6.0.22",
41 | "@mantine/form": "^7.13.5",
42 | "@mantine/hooks": "^6.0.22",
43 | "@mantine/modals": "^6.0.22",
44 | "@mantine/notifications": "^6.0.22",
45 | "@mantine/nprogress": "^6.0.22",
46 | "@mantine/prism": "^6.0.22",
47 | "@mantine/spotlight": "^6.0.22",
48 | "@mantine/tiptap": "^6.0.22",
49 | "@monaco-editor/react": "^4.6.0",
50 | "@nanostores/persistent": "^0.9.1",
51 | "@nanostores/react": "^0.7.3",
52 | "@parcel/watcher": "^2.5.0",
53 | "@tabler/icons-react": "^2.47.0",
54 | "@tanstack/eslint-plugin-query": "^5.60.1",
55 | "@tanstack/react-query": "^4.36.1",
56 | "@tanstack/react-query-devtools": "^4.36.1",
57 | "@tiptap/extension-link": "^2.9.1",
58 | "@tiptap/react": "^2.9.1",
59 | "@tiptap/starter-kit": "^2.9.1",
60 | "@types/node": "^20.17.6",
61 | "@types/react": "^18.3.12",
62 | "@types/react-copy-to-clipboard": "^5.0.7",
63 | "@types/react-dom": "^18.3.1",
64 | "@types/urijs": "^1.19.25",
65 | "@types/uuid": "^9.0.8",
66 | "@typescript-eslint/eslint-plugin": "^6.21.0",
67 | "@typescript-eslint/parser": "^6.21.0",
68 | "@vitejs/plugin-react-swc": "^3.7.1",
69 | "@vitest/ui": "^0.34.7",
70 | "dayjs": "^1.11.13",
71 | "embla-carousel-react": "8.0.0-rc14",
72 | "eslint": "^8.57.1",
73 | "eslint-config-prettier": "^9.1.0",
74 | "eslint-import-resolver-typescript": "^3.6.3",
75 | "eslint-plugin-import": "^2.31.0",
76 | "eslint-plugin-prettier": "5.0.1",
77 | "eslint-plugin-react": "^7.37.2",
78 | "eslint-plugin-react-hooks": "^4.6.2",
79 | "execa": "^8.0.1",
80 | "framer-motion": "^10.18.0",
81 | "graphiql": "^3.7.2",
82 | "graphql": "^16.9.0",
83 | "graphql-request": "^6.1.0",
84 | "husky": "^8.0.3",
85 | "i18next": "^23.16.5",
86 | "i18next-browser-languagedetector": "^7.2.1",
87 | "immer": "^10.1.1",
88 | "js-base64": "^3.7.7",
89 | "lint-staged": "^15.2.10",
90 | "mantine-datatable": "^6.0.8",
91 | "monaco-editor": "^0.44.0",
92 | "monaco-themes": "^0.4.4",
93 | "nanostores": "^0.9.5",
94 | "prettier": "^3.3.3",
95 | "qrcode.react": "^3.2.0",
96 | "react": "^18.3.1",
97 | "react-copy-to-clipboard": "^5.1.0",
98 | "react-dom": "^18.3.1",
99 | "react-i18next": "^13.5.0",
100 | "react-router": "^6.28.0",
101 | "react-router-dom": "^6.28.0",
102 | "simple-git": "^3.27.0",
103 | "sort-package-json": "^2.10.1",
104 | "typescript": "^5.6.3",
105 | "urijs": "^1.19.11",
106 | "vite": "^4.5.5",
107 | "vite-plugin-environment": "^1.1.3",
108 | "vitest": "^0.34.6",
109 | "zod": "^3.23.8"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/public/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeuniverse/daed/c3588a904c932d1fc83ee51096761776003fc25c/public/logo.webp
--------------------------------------------------------------------------------
/publish.Dockerfile:
--------------------------------------------------------------------------------
1 | # ATTENTION This part below is for publishing purpose only
2 |
3 | ARG DAED_VERSION
4 |
5 | FROM golang:1.22-bookworm as build
6 |
7 | RUN \
8 | apt-get update; apt-get install -y git make llvm-15 clang-15; \
9 | apt-get clean autoclean && apt-get autoremove -y && rm -rf /var/lib/{apt,dpkg,cache,log}/
10 |
11 | # build bundle process
12 | ENV CGO_ENABLED=0
13 | ENV CLANG=clang-15
14 | ARG DAED_VERSION
15 |
16 | WORKDIR /build
17 |
18 | COPY ./dist/ ./web/
19 | COPY ./wing/ ./wing/
20 |
21 | WORKDIR /build/wing
22 |
23 | RUN make APPNAME=daed VERSION=$DAED_VERSION OUTPUT=daed WEB_DIST=/build/web/ bundle
24 |
25 |
26 | FROM alpine as prod
27 |
28 | LABEL org.opencontainers.image.source=https://github.com/daeuniverse/daed
29 |
30 | RUN mkdir -p /usr/local/share/daed/
31 | RUN mkdir -p /etc/daed/
32 | RUN wget -O /usr/local/share/daed/geoip.dat https://github.com/v2rayA/dist-v2ray-rules-dat/raw/master/geoip.dat; \
33 | wget -O /usr/local/share/daed/geosite.dat https://github.com/v2rayA/dist-v2ray-rules-dat/raw/master/geosite.dat
34 | COPY --from=build /build/wing/daed /usr/local/bin
35 |
36 | EXPOSE 2023
37 |
38 | CMD ["daed", "run", "-c", "/etc/daed"]
39 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | ColorScheme,
4 | ColorSchemeProvider,
5 | createEmotionCache,
6 | MantineProvider,
7 | MantineThemeOverride,
8 | ScrollArea,
9 | } from '@mantine/core'
10 | import { useColorScheme } from '@mantine/hooks'
11 | import { ModalsProvider } from '@mantine/modals'
12 | import { Notifications } from '@mantine/notifications'
13 | import { useStore } from '@nanostores/react'
14 | import { useCallback, useEffect, useState } from 'react'
15 |
16 | import { QueryProvider } from '~/contexts'
17 | import { Router } from '~/Router'
18 | import { appStateAtom, colorSchemeAtom } from '~/store'
19 |
20 | const emotionCache = createEmotionCache({ key: 'mantine' })
21 |
22 | export const App = () => {
23 | const appState = useStore(appStateAtom)
24 | const preferredColorScheme = useColorScheme()
25 | const [colorScheme, setColorScheme] = useState(preferredColorScheme)
26 | const toggleColorScheme = useCallback(
27 | (value?: ColorScheme) => {
28 | const toScheme = value || (colorScheme === 'dark' ? 'light' : 'dark')
29 | setColorScheme(toScheme)
30 | appStateAtom.setKey('preferredColorScheme', toScheme)
31 | },
32 | [colorScheme],
33 | )
34 |
35 | useEffect(() => {
36 | const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)')
37 | const onDarkModeChange = (e: MediaQueryListEvent) => toggleColorScheme(e.matches ? 'dark' : 'light')
38 |
39 | darkModePreference.addEventListener('change', onDarkModeChange)
40 |
41 | return () => darkModePreference.removeEventListener('change', onDarkModeChange)
42 | }, [toggleColorScheme])
43 |
44 | useEffect(() => {
45 | setColorScheme(appState.preferredColorScheme || preferredColorScheme)
46 | }, [setColorScheme, preferredColorScheme, appState.preferredColorScheme])
47 |
48 | useEffect(() => {
49 | colorSchemeAtom.set(colorScheme)
50 | }, [colorScheme])
51 |
52 | const themeObject: MantineThemeOverride = {
53 | colorScheme,
54 | fontFamily: 'Fira Sans, Monaco, Consolas, sans-serif',
55 | fontFamilyMonospace: 'Source Code Pro, Monaco, Consolas, monospace',
56 | primaryColor: 'violet',
57 | cursorType: 'pointer',
58 | components: {
59 | Stack: { defaultProps: { spacing: 'sm' } },
60 | Group: { defaultProps: { spacing: 'sm' } },
61 | Button: { defaultProps: { uppercase: true } },
62 | ActionIcon: { defaultProps: { size: 'sm' } },
63 | Tooltip: { defaultProps: { withArrow: true } },
64 | HoverCard: { defaultProps: { withArrow: true } },
65 | Modal: {
66 | defaultProps: {
67 | size: 'lg',
68 | radius: 'md',
69 | centered: true,
70 | scrollAreaComponent: ScrollArea.Autosize,
71 | },
72 | },
73 | ModalHeader: {
74 | defaultProps: (theme) => ({
75 | bg: colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[4],
76 | }),
77 | },
78 | ModalTitle: {
79 | defaultProps: {
80 | color: 'white',
81 | },
82 | },
83 | Drawer: {
84 | defaultProps: {
85 | size: 'lg',
86 | scrollAreaComponent: ScrollArea.Autosize,
87 | },
88 | },
89 | Menu: {
90 | styles: {
91 | label: {
92 | textTransform: 'uppercase',
93 | },
94 | },
95 | },
96 | Select: {
97 | defaultProps: {
98 | withinPortal: true,
99 | size: 'xs',
100 | },
101 | },
102 | MultiSelect: { defaultProps: { size: 'xs' } },
103 | Switch: { defaultProps: { size: 'xs' } },
104 | Checkbox: { defaultProps: { size: 'xs' } },
105 | Radio: { defaultProps: { size: 'xs' } },
106 | RadioGroup: { defaultProps: { size: 'xs' } },
107 | TextInput: { defaultProps: { size: 'xs' } },
108 | NumberInput: { defaultProps: { size: 'xs' } },
109 | },
110 | }
111 |
112 | return (
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | )
126 | }
127 |
--------------------------------------------------------------------------------
/src/Router.tsx:
--------------------------------------------------------------------------------
1 | import { createGraphiQLFetcher } from '@graphiql/toolkit'
2 | import { useStore } from '@nanostores/react'
3 | import { GraphiQL } from 'graphiql'
4 | import { BrowserRouter, HashRouter, Route, Routes } from 'react-router-dom'
5 |
6 | import { ExperimentPage, MainLayout, OrchestratePage, SetupPage } from '~/pages'
7 | import { endpointURLAtom } from '~/store'
8 |
9 | export const Router = () => {
10 | const endpointURL = useStore(endpointURLAtom)
11 | const RouterType = import.meta.env.DEV ? BrowserRouter : HashRouter
12 |
13 | return (
14 |
15 |
16 | }>
17 | } />
18 | } />
19 |
20 |
21 | } />
22 |
23 | {endpointURL && (
24 |
32 | }
33 | />
34 | )}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/apis/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mutation'
2 | export * from './query'
3 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/HTTPForm.tsx:
--------------------------------------------------------------------------------
1 | import { NumberInput, Select, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { useTranslation } from 'react-i18next'
4 | import { z } from 'zod'
5 |
6 | import { FormActions } from '~/components/FormActions'
7 | import { DEFAULT_HTTP_FORM_VALUES, httpSchema } from '~/constants'
8 | import { GenerateURLParams, generateURL } from '~/utils'
9 |
10 | export const HTTPForm = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { onSubmit, getInputProps, reset } = useForm & { protocol: 'http' | 'https' }>({
13 | initialValues: {
14 | protocol: 'http',
15 | ...DEFAULT_HTTP_FORM_VALUES,
16 | },
17 | validate: zodResolver(httpSchema),
18 | })
19 |
20 | const handleSubmit = onSubmit((values) => {
21 | const generateURLParams: GenerateURLParams = {
22 | protocol: values.protocol,
23 | host: values.host,
24 | port: values.port,
25 | hash: values.name,
26 | }
27 |
28 | if (values.username && values.password) {
29 | Object.assign(generateURLParams, {
30 | username: values.username,
31 | password: values.password,
32 | })
33 | }
34 |
35 | return onLinkGeneration(generateURL(generateURLParams))
36 | })
37 |
38 | return (
39 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/Hysteria2Form.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox, NumberInput, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { useTranslation } from 'react-i18next'
4 | import { z } from 'zod'
5 |
6 | import { FormActions } from '~/components/FormActions'
7 | import { DEFAULT_HYSTERIA2_FORM_VALUES, hysteria2Schema } from '~/constants'
8 | import { generateHysteria2URL } from '~/utils'
9 |
10 | export const Hysteria2Form = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { onSubmit, getInputProps, reset } = useForm>({
13 | initialValues: DEFAULT_HYSTERIA2_FORM_VALUES,
14 | validate: zodResolver(hysteria2Schema),
15 | })
16 |
17 | const handleSubmit = onSubmit((values) => {
18 | /* hysteria2://[auth@]hostname[:port]/?[key=value]&[key=value]... */
19 | const query = {
20 | obfs: values.obfs,
21 | obfsPassword: values.obfsPassword,
22 | sni: values.sni,
23 | insecure: values.allowInsecure ? 1 : 0,
24 | pinSHA256: values.pinSHA256,
25 | }
26 |
27 | return onLinkGeneration(
28 | generateHysteria2URL({
29 | protocol: 'hysteria2',
30 | auth: values.auth,
31 | host: values.server,
32 | port: values.port,
33 | params: query,
34 | }),
35 | )
36 | })
37 |
38 | return (
39 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/JuicityForm.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox, NumberInput, Select, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { useTranslation } from 'react-i18next'
4 | import { z } from 'zod'
5 |
6 | import { FormActions } from '~/components/FormActions'
7 | import { DEFAULT_JUICITY_FORM_VALUES, juicitySchema } from '~/constants'
8 | import { generateURL } from '~/utils'
9 |
10 | export const JuicityForm = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { onSubmit, getInputProps, reset } = useForm>({
13 | initialValues: DEFAULT_JUICITY_FORM_VALUES,
14 | validate: zodResolver(juicitySchema),
15 | })
16 |
17 | const handleSubmit = onSubmit((values) => {
18 | const query = {
19 | congestion_control: values.congestion_control,
20 | pinned_certchain_sha256: values.pinned_certchain_sha256,
21 | sni: values.sni,
22 | allow_insecure: values.allowInsecure,
23 | }
24 |
25 | return onLinkGeneration(
26 | generateURL({
27 | protocol: 'juicity',
28 | username: values.uuid,
29 | password: values.password,
30 | host: values.server,
31 | port: values.port,
32 | hash: values.name,
33 | params: query,
34 | }),
35 | )
36 | })
37 |
38 | return (
39 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/SSForm.tsx:
--------------------------------------------------------------------------------
1 | import { NumberInput, Select, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { Base64 } from 'js-base64'
4 | import { useTranslation } from 'react-i18next'
5 | import { z } from 'zod'
6 |
7 | import { FormActions } from '~/components/FormActions'
8 | import { DEFAULT_SS_FORM_VALUES, ssSchema } from '~/constants'
9 |
10 | export const SSForm = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { values, onSubmit, getInputProps, reset } = useForm>({
13 | initialValues: DEFAULT_SS_FORM_VALUES,
14 | validate: zodResolver(ssSchema),
15 | })
16 |
17 | const handleSubmit = onSubmit((values) => {
18 | /* ss://BASE64(method:password)@server:port#name */
19 | let link = `ss://${Base64.encode(`${values.method}:${values.password}`)}@${values.server}:${values.port}/`
20 |
21 | if (values.plugin) {
22 | const plugin: string[] = [values.plugin]
23 |
24 | if (values.plugin === 'v2ray-plugin') {
25 | if (values.tls) {
26 | plugin.push('tls')
27 | }
28 |
29 | if (values.mode !== 'websocket') {
30 | plugin.push('mode=' + values.mode)
31 | }
32 |
33 | if (values.host) {
34 | plugin.push('host=' + values.host)
35 | }
36 |
37 | if (values.path) {
38 | if (!values.path.startsWith('/')) {
39 | values.path = '/' + values.path
40 | }
41 |
42 | plugin.push('path=' + values.path)
43 | }
44 |
45 | if (values.impl) {
46 | plugin.push('impl=' + values.impl)
47 | }
48 | } else {
49 | plugin.push('obfs=' + values.obfs)
50 | plugin.push('obfs-host=' + values.host)
51 |
52 | if (values.obfs === 'http') {
53 | plugin.push('obfs-path=' + values.path)
54 | }
55 |
56 | if (values.impl) {
57 | plugin.push('impl=' + values.impl)
58 | }
59 | }
60 |
61 | link += `?plugin=${encodeURIComponent(plugin.join(';'))}`
62 | }
63 |
64 | link += values.name.length ? `#${encodeURIComponent(values.name)}` : ''
65 |
66 | return onLinkGeneration(link)
67 | })
68 |
69 | return (
70 |
150 | )
151 | }
152 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/SSRForm.tsx:
--------------------------------------------------------------------------------
1 | import { NumberInput, Select, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { Base64 } from 'js-base64'
4 | import { useTranslation } from 'react-i18next'
5 | import { z } from 'zod'
6 |
7 | import { FormActions } from '~/components/FormActions'
8 | import { DEFAULT_SSR_FORM_VALUES, ssrSchema } from '~/constants'
9 |
10 | export const SSRForm = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { values, onSubmit, getInputProps, reset } = useForm>({
13 | initialValues: DEFAULT_SSR_FORM_VALUES,
14 | validate: zodResolver(ssrSchema),
15 | })
16 |
17 | const handleSubmit = onSubmit((values) => {
18 | /* ssr://server:port:proto:method:obfs:URLBASE64(password)/?remarks=URLBASE64(remarks)&protoparam=URLBASE64(protoparam)&obfsparam=URLBASE64(obfsparam)) */
19 | return onLinkGeneration(
20 | `ssr://${Base64.encode(
21 | `${values.server}:${values.port}:${values.proto}:${values.method}:${values.obfs}:${Base64.encodeURI(
22 | values.password,
23 | )}/?remarks=${Base64.encodeURI(values.name)}&protoparam=${Base64.encodeURI(
24 | values.protoParam,
25 | )}&obfsparam=${Base64.encodeURI(values.obfsParam)}`,
26 | )}`,
27 | )
28 | })
29 |
30 | return (
31 |
107 | )
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/Socks5Form.tsx:
--------------------------------------------------------------------------------
1 | import { NumberInput, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { useTranslation } from 'react-i18next'
4 | import { z } from 'zod'
5 |
6 | import { FormActions } from '~/components/FormActions'
7 | import { DEFAULT_SOCKS5_FORM_VALUES, socks5Schema } from '~/constants'
8 | import { GenerateURLParams, generateURL } from '~/utils'
9 |
10 | export const Socks5Form = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { onSubmit, getInputProps, reset } = useForm>({
13 | initialValues: DEFAULT_SOCKS5_FORM_VALUES,
14 | validate: zodResolver(socks5Schema),
15 | })
16 |
17 | const handleSubmit = onSubmit((values) => {
18 | const generateURLParams: GenerateURLParams = {
19 | protocol: 'socks5',
20 | host: values.host,
21 | port: values.port,
22 | hash: values.name,
23 | }
24 |
25 | if (values.username && values.password) {
26 | Object.assign(generateURLParams, {
27 | username: values.username,
28 | password: values.password,
29 | })
30 | }
31 |
32 | return onLinkGeneration(generateURL(generateURLParams))
33 | })
34 |
35 | return (
36 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/TrojanForm.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox, NumberInput, Select, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { useTranslation } from 'react-i18next'
4 | import { z } from 'zod'
5 |
6 | import { FormActions } from '~/components/FormActions'
7 | import { DEFAULT_TROJAN_FORM_VALUES, trojanSchema } from '~/constants'
8 | import { generateURL } from '~/utils'
9 |
10 | export const TrojanForm = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { values, onSubmit, getInputProps, reset } = useForm>({
13 | initialValues: DEFAULT_TROJAN_FORM_VALUES,
14 | validate: zodResolver(trojanSchema),
15 | })
16 |
17 | const handleSubmit = onSubmit((values) => {
18 | const query: Record = {
19 | allowInsecure: values.allowInsecure,
20 | }
21 |
22 | if (values.peer !== '') {
23 | query.sni = values.peer
24 | }
25 |
26 | let protocol = 'trojan'
27 |
28 | if (values.method !== 'origin' || values.obfs !== 'none') {
29 | protocol = 'trojan-go'
30 | query.type = values.obfs === 'none' ? 'original' : 'ws'
31 |
32 | if (values.method === 'shadowsocks') {
33 | query.encryption = `ss;${values.ssCipher};${values.ssPassword}`
34 | }
35 |
36 | if (query.type === 'ws') {
37 | query.host = values.host || ''
38 | query.path = values.path || '/'
39 | }
40 |
41 | delete query.allowInsecure
42 | }
43 |
44 | return onLinkGeneration(
45 | generateURL({
46 | protocol,
47 | username: values.password,
48 | host: values.server,
49 | port: values.port,
50 | hash: values.name,
51 | params: query,
52 | }),
53 | )
54 | })
55 |
56 | return (
57 |
117 | )
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/TuicForm.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox, NumberInput, Select, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { useTranslation } from 'react-i18next'
4 | import { z } from 'zod'
5 |
6 | import { FormActions } from '~/components/FormActions'
7 | import { DEFAULT_TUIC_FORM_VALUES, tuicSchema } from '~/constants'
8 | import { generateURL } from '~/utils'
9 |
10 | export const TuicForm = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
11 | const { t } = useTranslation()
12 | const { onSubmit, getInputProps, reset } = useForm>({
13 | initialValues: DEFAULT_TUIC_FORM_VALUES,
14 | validate: zodResolver(tuicSchema),
15 | })
16 |
17 | const handleSubmit = onSubmit((values) => {
18 | const query = {
19 | congestion_control: values.congestion_control,
20 | alpn: values.alpn,
21 | sni: values.sni,
22 | allow_insecure: values.allowInsecure,
23 | disable_sni: values.disable_sni,
24 | udp_relay_mode: values.udp_relay_mode,
25 | }
26 |
27 | return onLinkGeneration(
28 | generateURL({
29 | protocol: 'tuic',
30 | username: values.uuid,
31 | password: values.password,
32 | host: values.server,
33 | port: values.port,
34 | hash: values.name,
35 | params: query,
36 | }),
37 | )
38 | })
39 |
40 | return (
41 |
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/V2rayForm.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox, NumberInput, Select, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { Base64 } from 'js-base64'
4 | import { useTranslation } from 'react-i18next'
5 | import { z } from 'zod'
6 |
7 | import { FormActions } from '~/components/FormActions'
8 | import { DEFAULT_V2RAY_FORM_VALUES, v2raySchema } from '~/constants'
9 | import { generateURL } from '~/utils'
10 |
11 | export const V2rayForm = ({ onLinkGeneration }: { onLinkGeneration: (link: string) => void }) => {
12 | const { t } = useTranslation()
13 | const { values, onSubmit, getInputProps, reset } = useForm<
14 | z.infer & { protocol: 'vless' | 'vmess' }
15 | >({
16 | initialValues: { protocol: 'vmess', ...DEFAULT_V2RAY_FORM_VALUES },
17 | validate: zodResolver(v2raySchema),
18 | })
19 |
20 | const handleSubmit = onSubmit((values) => {
21 | const { protocol, net, tls, path, host, type, sni, flow, allowInsecure, alpn, id, add, port, ps } = values
22 |
23 | if (protocol === 'vless') {
24 | const params: Record = {
25 | type: net,
26 | security: tls,
27 | path,
28 | host,
29 | headerType: type,
30 | sni,
31 | flow,
32 | allowInsecure,
33 | }
34 |
35 | if (alpn !== '') params.alpn = alpn
36 |
37 | if (net === 'grpc') params.serviceName = path
38 |
39 | if (net === 'kcp') params.seed = path
40 |
41 | return onLinkGeneration(
42 | generateURL({
43 | protocol,
44 | username: id,
45 | host: add,
46 | port,
47 | hash: ps,
48 | params,
49 | }),
50 | )
51 | }
52 |
53 | if (protocol === 'vmess') {
54 | const body: Record = structuredClone(values)
55 |
56 | switch (net) {
57 | case 'kcp':
58 | case 'tcp':
59 | default:
60 | body.type = ''
61 | }
62 |
63 | switch (body.net) {
64 | case 'ws':
65 | // 不做任何操作,直接跳过
66 | break;
67 | case 'h2':
68 | case 'grpc':
69 | case 'kcp':
70 | default:
71 | if (body.net === 'tcp' && body.type === 'http') {
72 | break
73 | }
74 |
75 | body.path = ''
76 | }
77 |
78 | if (!(body.protocol === 'vless' && body.tls === 'xtls')) {
79 | delete body.flow
80 | }
81 |
82 | return onLinkGeneration('vmess://' + Base64.encode(JSON.stringify(body)))
83 | }
84 | })
85 |
86 | return (
87 |
207 | )
208 | }
209 |
--------------------------------------------------------------------------------
/src/components/ConfigureNodeFormModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { MantineProvider, Modal, Stack, Tabs, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { useTranslation } from 'react-i18next'
4 | import { z } from 'zod'
5 |
6 | import { useImportNodesMutation } from '~/apis'
7 |
8 | import { HTTPForm } from './HTTPForm'
9 | import { Hysteria2Form } from './Hysteria2Form'
10 | import { JuicityForm } from './JuicityForm'
11 | import { SSForm } from './SSForm'
12 | import { SSRForm } from './SSRForm'
13 | import { Socks5Form } from './Socks5Form'
14 | import { TrojanForm } from './TrojanForm'
15 | import { TuicForm } from './TuicForm'
16 | import { V2rayForm } from './V2rayForm'
17 |
18 | const schema = z.object({ tag: z.string().nonempty() })
19 |
20 | export const ConfigureNodeFormModal = ({ opened, onClose }: { opened: boolean; onClose: () => void }) => {
21 | const { t } = useTranslation()
22 | const importNodesMutation = useImportNodesMutation()
23 | const form = useForm>({
24 | initialValues: { tag: '' },
25 | validate: zodResolver(schema),
26 | })
27 |
28 | const onLinkGeneration = async (link: string) => {
29 | const { hasErrors } = form.validate()
30 |
31 | if (hasErrors) return
32 |
33 | await importNodesMutation.mutateAsync([
34 | {
35 | link,
36 | tag: form.values.tag,
37 | },
38 | ])
39 |
40 | onClose()
41 | }
42 |
43 | return (
44 |
45 |
46 |
47 |
61 |
62 |
63 | V2RAY
64 | SS
65 | SSR
66 | Trojan
67 | Juicity
68 | Hysteria2
69 | Tuic
70 | HTTP
71 | SOCKS5
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | )
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/DraggableResourceBadge.tsx:
--------------------------------------------------------------------------------
1 | import { useDraggable } from '@dnd-kit/core'
2 | import { ActionIcon, Badge, Text, Tooltip } from '@mantine/core'
3 | import { IconX } from '@tabler/icons-react'
4 |
5 | import { DraggableResourceType } from '~/constants'
6 |
7 | export const DraggableResourceBadge = ({
8 | id,
9 | name,
10 | type,
11 | nodeID,
12 | groupID,
13 | subscriptionID,
14 | onRemove,
15 | dragDisabled,
16 | children,
17 | }: {
18 | id: string
19 | name: string
20 | type: DraggableResourceType
21 | nodeID?: string
22 | groupID?: string
23 | subscriptionID?: string
24 | onRemove?: () => void
25 | dragDisabled?: boolean
26 | children?: React.ReactNode
27 | }) => {
28 | const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
29 | id,
30 | data: {
31 | type,
32 | nodeID,
33 | groupID,
34 | subscriptionID,
35 | },
36 | disabled: dragDisabled,
37 | })
38 |
39 | return (
40 | {children}}>
41 |
47 |
48 |
49 | )
50 | }
51 | style={{
52 | zIndex: isDragging ? 10 : 0,
53 | cursor: isDragging ? 'grabbing' : 'grab',
54 | }}
55 | opacity={isDragging ? 0.5 : undefined}
56 | >
57 |
58 | {name}
59 |
60 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/DraggableResourceCard.tsx:
--------------------------------------------------------------------------------
1 | import { useDraggable } from '@dnd-kit/core'
2 | import { ActionIcon, Badge, Card, Group, Text } from '@mantine/core'
3 | import { modals } from '@mantine/modals'
4 | import { IconTrash } from '@tabler/icons-react'
5 | import React from 'react'
6 | import { useTranslation } from 'react-i18next'
7 |
8 | import { DraggableResourceType } from '~/constants'
9 |
10 | export const DraggableResourceCard = ({
11 | id,
12 | nodeID,
13 | subscriptionID,
14 | type,
15 | name,
16 | leftSection,
17 | onRemove,
18 | actions,
19 | children,
20 | }: {
21 | id: string
22 | nodeID?: string
23 | subscriptionID?: string
24 | type: DraggableResourceType
25 | name: React.ReactNode
26 | leftSection?: React.ReactNode
27 | onRemove: () => void
28 | actions?: React.ReactNode
29 | children: React.ReactNode
30 | }) => {
31 | const { t } = useTranslation()
32 | const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id, data: { type, nodeID, subscriptionID } })
33 |
34 | return (
35 |
44 |
45 |
46 | {leftSection}
47 |
48 |
57 | {name}
58 |
59 |
60 |
61 | {actions}
62 |
63 | {
67 | modals.openConfirmModal({
68 | title: t('actions.remove'),
69 | labels: {
70 | cancel: t('confirmModal.cancel'),
71 | confirm: t('confirmModal.confirm'),
72 | },
73 | children: t('confirmModal.removeConfirmDescription'),
74 | onConfirm: onRemove,
75 | })
76 | }}
77 | >
78 |
79 |
80 |
81 |
82 |
83 |
84 | {children}
85 |
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/DroppableGroupCard.tsx:
--------------------------------------------------------------------------------
1 | import { useDroppable } from '@dnd-kit/core'
2 | import { ActionIcon, Card, Group, Title } from '@mantine/core'
3 | import { modals } from '@mantine/modals'
4 | import { IconTrash } from '@tabler/icons-react'
5 | import { useTranslation } from 'react-i18next'
6 |
7 | export const DroppableGroupCard = ({
8 | id,
9 | name,
10 | onRemove,
11 | actions,
12 | children,
13 | }: {
14 | id: string
15 | name: string
16 | onRemove?: () => void
17 | actions?: React.ReactNode
18 | children?: React.ReactNode
19 | }) => {
20 | const { t } = useTranslation()
21 | const { isOver, setNodeRef } = useDroppable({ id })
22 |
23 | return (
24 |
33 |
34 |
35 | {name}
36 |
37 |
38 | {actions}
39 |
40 | {onRemove && (
41 | {
45 | modals.openConfirmModal({
46 | title: t('actions.remove'),
47 | labels: {
48 | cancel: t('confirmModal.cancel'),
49 | confirm: t('confirmModal.confirm'),
50 | },
51 | children: t('confirmModal.removeConfirmDescription'),
52 | onConfirm: onRemove,
53 | })
54 | }}
55 | >
56 |
57 |
58 | )}
59 |
60 |
61 |
62 |
63 | {children && (
64 |
65 | {children}
66 |
67 | )}
68 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/ExpandedTableRow.tsx:
--------------------------------------------------------------------------------
1 | import { Divider, Group, Stack, Text } from '@mantine/core'
2 | import { Fragment } from 'react'
3 |
4 | export const ExpandedTableRow = ({ data }: { data: { name: string; value: string }[] }) => (
5 |
6 | {data.map(({ name, value }, i) => {
7 | return (
8 |
9 |
10 | {name}:
11 | {value}
12 |
13 |
14 | {i !== data.length - 1 && }
15 |
16 | )
17 | })}
18 |
19 | )
20 |
--------------------------------------------------------------------------------
/src/components/FormActions.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Group } from '@mantine/core'
2 | import { useTranslation } from 'react-i18next'
3 |
4 | export const FormActions = ({ loading, reset }: { loading?: boolean; reset?: () => void }) => {
5 | const { t } = useTranslation()
6 |
7 | return (
8 |
9 |
12 |
13 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/GroupFormModal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, Select, Stack, TextInput } from '@mantine/core'
2 | import { UseFormReturnType, useForm, zodResolver } from '@mantine/form'
3 | import { forwardRef, useImperativeHandle, useState } from 'react'
4 | import { useTranslation } from 'react-i18next'
5 | import { z } from 'zod'
6 |
7 | import { useCreateGroupMutation, useGroupSetPolicyMutation } from '~/apis'
8 | import { FormActions } from '~/components/FormActions'
9 | import { DEFAULT_GROUP_POLICY } from '~/constants'
10 | import { Policy } from '~/schemas/gql/graphql'
11 |
12 | import { SelectItemWithDescription } from './SelectItemWithDescription'
13 |
14 | const schema = z.object({
15 | name: z.string().nonempty(),
16 | policy: z.nativeEnum(Policy),
17 | })
18 |
19 | export type GroupFormModalRef = {
20 | form: UseFormReturnType>
21 | setEditingID: (id: string) => void
22 | initOrigins: (origins: z.infer) => void
23 | }
24 |
25 | export const GroupFormModal = forwardRef(({ opened, onClose }: { opened: boolean; onClose: () => void }, ref) => {
26 | const { t } = useTranslation()
27 | const [editingID, setEditingID] = useState()
28 | const [origins, setOrigins] = useState>()
29 | const form = useForm>({
30 | validate: zodResolver(schema),
31 | initialValues: {
32 | name: '',
33 | policy: DEFAULT_GROUP_POLICY,
34 | },
35 | })
36 |
37 | const initOrigins = (origins: z.infer) => {
38 | form.setValues(origins)
39 | setOrigins(origins)
40 | }
41 |
42 | useImperativeHandle(ref, () => ({
43 | form,
44 | setEditingID,
45 | initOrigins,
46 | }))
47 |
48 | const createGroupMutation = useCreateGroupMutation()
49 | const groupSetPolicyMutation = useGroupSetPolicyMutation()
50 |
51 | const policyData = [
52 | {
53 | label: Policy.MinMovingAvg,
54 | value: Policy.MinMovingAvg,
55 | description: t('descriptions.group.MinMovingAvg'),
56 | },
57 | {
58 | label: Policy.MinAvg10,
59 | value: Policy.MinAvg10,
60 | description: t('descriptions.group.MinAvg10'),
61 | },
62 | {
63 | label: Policy.Min,
64 | value: Policy.Min,
65 | description: t('descriptions.group.Min'),
66 | },
67 | {
68 | label: Policy.Random,
69 | value: Policy.Random,
70 | description: t('descriptions.group.Random'),
71 | },
72 | {
73 | label: Policy.Fixed,
74 | value: Policy.Fixed,
75 | description: t('descriptions.group.Fixed'),
76 | },
77 | ]
78 |
79 | return (
80 |
81 |
125 |
126 | )
127 | })
128 |
--------------------------------------------------------------------------------
/src/components/ImportResourceFormModal.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon, Flex, Group, Modal, TextInput } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { randomId } from '@mantine/hooks'
4 | import { IconMinus, IconPlus } from '@tabler/icons-react'
5 | import { useTranslation } from 'react-i18next'
6 | import { z } from 'zod'
7 |
8 | import { FormActions } from '~/components/FormActions'
9 |
10 | const schema = z.object({
11 | resources: z
12 | .array(
13 | z.object({
14 | id: z.string(),
15 | link: z.string(),
16 | tag: z.string().min(1),
17 | }),
18 | )
19 | .nonempty(),
20 | })
21 |
22 | export const ImportResourceFormModal = ({
23 | title,
24 | opened,
25 | onClose,
26 | handleSubmit,
27 | }: {
28 | title: string
29 | opened: boolean
30 | onClose: () => void
31 | handleSubmit: (values: z.infer) => Promise
32 | }) => {
33 | const { t } = useTranslation()
34 | const form = useForm>({
35 | validate: zodResolver(schema),
36 | initialValues: {
37 | resources: [
38 | {
39 | id: randomId(),
40 | link: '',
41 | tag: '',
42 | },
43 | ],
44 | },
45 | })
46 |
47 | return (
48 |
49 |
103 |
104 | )
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/PlainTextFormModal.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Input, Modal, Stack, TextInput } from '@mantine/core'
2 | import { UseFormReturnType, useForm, zodResolver } from '@mantine/form'
3 | import { Editor } from '@monaco-editor/react'
4 | import { useStore } from '@nanostores/react'
5 | import { forwardRef, useImperativeHandle, useState } from 'react'
6 | import { useTranslation } from 'react-i18next'
7 | import { z } from 'zod'
8 |
9 | import { EDITOR_OPTIONS, EDITOR_THEME_DARK, EDITOR_THEME_LIGHT } from '~/constants'
10 | import { colorSchemeAtom } from '~/store'
11 |
12 | import { FormActions } from './FormActions'
13 |
14 | const schema = z.object({
15 | name: z.string().nonempty(),
16 | text: z.string().nonempty(),
17 | })
18 |
19 | export type PlainTextgFormModalRef = {
20 | form: UseFormReturnType>
21 | editingID: string
22 | setEditingID: (id: string) => void
23 | initOrigins: (origins: z.infer) => void
24 | }
25 |
26 | export const PlainTextFormModal = forwardRef(
27 | (
28 | {
29 | title,
30 | opened,
31 | onClose,
32 | handleSubmit,
33 | }: {
34 | title: string
35 | opened: boolean
36 | onClose: () => void
37 | handleSubmit: (values: z.infer) => Promise
38 | },
39 | ref,
40 | ) => {
41 | const { t } = useTranslation()
42 | const colorScheme = useStore(colorSchemeAtom)
43 | const [editingID, setEditingID] = useState()
44 | const [origins, setOrigins] = useState>()
45 | const form = useForm>({
46 | validate: zodResolver(schema),
47 | initialValues: {
48 | name: '',
49 | text: '',
50 | },
51 | })
52 |
53 | const initOrigins = (origins: z.infer) => {
54 | form.setValues(origins)
55 | setOrigins(origins)
56 | }
57 |
58 | useImperativeHandle(ref, () => ({
59 | form,
60 | editingID,
61 | setEditingID,
62 | initOrigins,
63 | }))
64 |
65 | return (
66 |
67 |
68 |
69 |
70 |
71 |
72 | {title}
73 |
74 |
75 |
76 |
77 |
114 |
115 |
116 |
117 |
118 | )
119 | },
120 | )
121 |
--------------------------------------------------------------------------------
/src/components/QRCodeModal.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon, Badge, Flex, Group, Modal } from '@mantine/core'
2 | import { IconCheck, IconCopy } from '@tabler/icons-react'
3 | import { QRCodeCanvas } from 'qrcode.react'
4 | import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
5 | import { CopyToClipboard } from 'react-copy-to-clipboard'
6 |
7 | type Props = {
8 | name: string
9 | link: string
10 | }
11 |
12 | export type QRCodeModalRef = {
13 | props: Props
14 | setProps: (props: Props) => void
15 | }
16 |
17 | export const QRCodeModal = forwardRef(({ opened, onClose }: { opened: boolean; onClose: () => void }, ref) => {
18 | const [props, setProps] = useState({
19 | name: '',
20 | link: '',
21 | })
22 |
23 | const [copied, setCopied] = useState(false)
24 |
25 | useEffect(() => {
26 | if (copied) {
27 | setTimeout(() => {
28 | setCopied(false)
29 | }, 500)
30 | }
31 | }, [copied])
32 |
33 | useImperativeHandle(ref, () => ({
34 | props,
35 | setProps,
36 | }))
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
53 | {props.link}
54 |
55 |
56 | setCopied(true)}>
57 | {copied ? : }
58 |
59 |
60 |
61 |
62 | )
63 | })
64 |
--------------------------------------------------------------------------------
/src/components/RenameFormModal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, SimpleGrid, Stack, TextInput, Title } from '@mantine/core'
2 | import { useForm, zodResolver } from '@mantine/form'
3 | import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'
4 | import { useTranslation } from 'react-i18next'
5 | import { z } from 'zod'
6 |
7 | import { useRenameConfigMutation, useRenameDNSMutation, useRenameGroupMutation, useRenameRoutingMutation } from '~/apis'
8 | import { RuleType } from '~/constants'
9 |
10 | import { FormActions } from './FormActions'
11 |
12 | const schema = z.object({
13 | name: z.string().nonempty(),
14 | })
15 |
16 | type Props = {
17 | id?: string
18 | type?: RuleType
19 | oldName?: string
20 | }
21 |
22 | export type RenameFormModalRef = {
23 | props: Props
24 | setProps: (props: Props) => void
25 | }
26 |
27 | export const RenameFormModal = forwardRef(
28 | (
29 | {
30 | opened,
31 | onClose,
32 | }: {
33 | opened: boolean
34 | onClose: () => void
35 | },
36 | ref,
37 | ) => {
38 | const { t } = useTranslation()
39 |
40 | const [props, setProps] = useState({})
41 | const { type, id } = props
42 |
43 | const ruleName = useMemo(() => {
44 | if (type === RuleType.config) {
45 | return t('config')
46 | }
47 |
48 | if (type === RuleType.dns) {
49 | return t('dns')
50 | }
51 |
52 | if (type === RuleType.routing) {
53 | return t('routing')
54 | }
55 | }, [type, t])
56 |
57 | useImperativeHandle(ref, () => ({
58 | props,
59 | setProps,
60 | }))
61 |
62 | const form = useForm>({
63 | validate: zodResolver(schema),
64 | initialValues: {
65 | name: '',
66 | },
67 | })
68 |
69 | const renameConfigMutation = useRenameConfigMutation()
70 | const renameDNSMutation = useRenameDNSMutation()
71 | const renameRoutingMutation = useRenameRoutingMutation()
72 | const renameGroupMutation = useRenameGroupMutation()
73 |
74 | return (
75 |
76 |
115 |
116 | )
117 | },
118 | )
119 |
--------------------------------------------------------------------------------
/src/components/Section.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon, Group, Stack, Title, createStyles } from '@mantine/core'
2 | import { IconPlus } from '@tabler/icons-react'
3 |
4 | const useStyles = createStyles((theme) => ({
5 | section: {
6 | border: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.gray[8] : theme.colors.gray[2]}`,
7 | borderRadius: theme.radius.sm,
8 | padding: theme.spacing.xs,
9 | boxShadow: theme.shadows.md,
10 | transition: 'background 300ms ease-in-out',
11 | },
12 | }))
13 |
14 | export const Section = ({
15 | title,
16 | icon,
17 | bordered,
18 | iconPlus,
19 | onCreate,
20 | actions,
21 | highlight,
22 | children,
23 | }: {
24 | title: string
25 | icon?: React.ReactNode
26 | bordered?: boolean
27 | iconPlus?: React.ReactNode
28 | onCreate: () => void
29 | actions?: React.ReactNode
30 | highlight?: boolean
31 | children: React.ReactNode
32 | }) => {
33 | const { classes, theme, cx } = useStyles()
34 |
35 | return (
36 |
40 |
41 |
42 | {icon}
43 |
44 |
45 | {title}
46 |
47 |
48 |
49 |
50 | {actions}
51 |
52 | {iconPlus || }
53 |
54 |
55 |
56 | {children}
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/SelectItemWithDescription.tsx:
--------------------------------------------------------------------------------
1 | import { Input, Stack, Text } from '@mantine/core'
2 | import { forwardRef } from 'react'
3 |
4 | interface SelectItemWithDescriptionProps extends React.ComponentPropsWithoutRef<'div'> {
5 | label: React.ReactNode
6 | description?: React.ReactNode
7 | selected?: boolean
8 | }
9 |
10 | export const SelectItemWithDescription = forwardRef(
11 | ({ label, description, ...props }, ref) => (
12 |
13 | {label}
14 |
15 | {description && (
16 |
21 | {description}
22 |
23 | )}
24 |
25 | ),
26 | )
27 |
--------------------------------------------------------------------------------
/src/components/SimpleCard.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon, Card, Group, Indicator, Modal, Title, UnstyledButton } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { modals } from '@mantine/modals'
4 | import { IconEye, IconTrash } from '@tabler/icons-react'
5 | import { Fragment } from 'react'
6 | import { useTranslation } from 'react-i18next'
7 |
8 | export const SimpleCard = ({
9 | name,
10 | selected,
11 | onSelect,
12 | onRemove,
13 | actions,
14 | children,
15 | }: {
16 | name: string
17 | selected: boolean
18 | onSelect?: () => void
19 | onRemove?: () => void
20 | actions?: React.ReactNode
21 | children: React.ReactNode
22 | }) => {
23 | const { t } = useTranslation()
24 |
25 | const [openedDetailsModal, { open: openDetailsModal, close: closeDetailsModal }] = useDisclosure(false)
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | {name}
35 |
36 |
37 |
38 | {actions}
39 |
40 |
41 |
42 |
43 |
44 | {!selected && onRemove && (
45 | {
49 | modals.openConfirmModal({
50 | title: t('actions.remove'),
51 | labels: {
52 | cancel: t('confirmModal.cancel'),
53 | confirm: t('confirmModal.confirm'),
54 | },
55 | children: t('confirmModal.removeConfirmDescription'),
56 | onConfirm: onRemove,
57 | })
58 | }}
59 | >
60 |
61 |
62 | )}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {children}
71 |
72 |
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/Table.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Group, Modal, createStyles } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { DataTable, DataTableColumn, DataTableRowExpansionProps } from 'mantine-datatable'
4 | import { useState } from 'react'
5 | import { useTranslation } from 'react-i18next'
6 |
7 | const useStyles = createStyles(() => ({
8 | header: {
9 | '&& th': {
10 | textTransform: 'uppercase',
11 | },
12 | },
13 | }))
14 |
15 | type Props = {
16 | fetching: boolean
17 | columns: DataTableColumn[]
18 | records: T[]
19 | createModalTitle?: string
20 | createModalContent?: (close: () => void) => React.ReactNode
21 | onRemove?: (records: T[]) => Promise
22 | isRecordSelectable?: (record: T, index: number) => boolean
23 | rowExpansion?: DataTableRowExpansionProps
24 | }
25 |
26 | export const Table = >({
27 | fetching,
28 | columns,
29 | records,
30 | isRecordSelectable,
31 | onRemove,
32 | rowExpansion,
33 | createModalTitle,
34 | createModalContent,
35 | }: Props) => {
36 | const { classes } = useStyles()
37 | const { t } = useTranslation()
38 | const [selectedRecords, onSelectedRecordsChange] = useState([])
39 | const [opened, { open, close }] = useDisclosure(false)
40 | const [removing, setRemoving] = useState(false)
41 |
42 | return (
43 |
44 |
45 |
46 |
47 |
64 |
65 |
66 |
83 |
84 |
85 | {createModalContent && createModalContent(close)}
86 |
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/UpdateSubscriptionAction.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon } from '@mantine/core'
2 | import { IconRefresh } from '@tabler/icons-react'
3 |
4 | import { useUpdateSubscriptionsMutation } from '~/apis'
5 |
6 | export const UpdateSubscriptionAction = ({ id, loading }: { id: string; loading?: boolean }) => {
7 | const updateSubscriptionsMutation = useUpdateSubscriptionsMutation()
8 |
9 | return (
10 | updateSubscriptionsMutation.mutate([id])}
14 | >
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfigFormModal'
2 | export * from './ConfigureNodeFormModal'
3 | export * from './DraggableResourceBadge'
4 | export * from './DraggableResourceCard'
5 | export * from './DroppableGroupCard'
6 | export * from './ExpandedTableRow'
7 | export * from './FormActions'
8 | export * from './GroupFormModal'
9 | export * from './Header'
10 | export * from './ImportResourceFormModal'
11 | export * from './PlainTextFormModal'
12 | export * from './QRCodeModal'
13 | export * from './RenameFormModal'
14 | export * from './Section'
15 | export * from './SelectItemWithDescription'
16 | export * from './SimpleCard'
17 | export * from './Table'
18 | export * from './UpdateSubscriptionAction'
19 |
--------------------------------------------------------------------------------
/src/constants/default.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | import { GlobalInput, Policy } from '~/schemas/gql/graphql'
4 |
5 | import { DialMode, LogLevel, TLSImplementation, TcpCheckHttpMethod, UTLSImitate } from './misc'
6 | import {
7 | httpSchema,
8 | hysteria2Schema,
9 | juicitySchema,
10 | socks5Schema,
11 | ssSchema,
12 | ssrSchema,
13 | trojanSchema,
14 | tuicSchema,
15 | v2raySchema,
16 | } from './schema'
17 |
18 | export const DEFAULT_ENDPOINT_URL = `${location.protocol}//${location.hostname}:2023/graphql`
19 |
20 | export const DEFAULT_LOG_LEVEL = LogLevel.info
21 | export const DEFAULT_TPROXY_PORT = 12345
22 | export const DEFAULT_TPROXY_PORT_PROTECT = true
23 | export const DEFAULT_SO_MARK_FROM_DAE = 0
24 | export const DEFAULT_ALLOW_INSECURE = false
25 | export const DEFAULT_CHECK_INTERVAL_SECONDS = 30
26 | export const DEFAULT_CHECK_TOLERANCE_MS = 0
27 | export const DEFAULT_SNIFFING_TIMEOUT_MS = 100
28 | export const DEFAULT_UDP_CHECK_DNS = ['dns.google:53', '8.8.8.8', '2001:4860:4860::8888']
29 | export const DEFAULT_TCP_CHECK_URL = ['http://cp.cloudflare.com', '1.1.1.1', '2606:4700:4700::1111']
30 | export const DEFAULT_DIAL_MODE = DialMode.domain
31 | export const DEFAULT_TCP_CHECK_HTTP_METHOD = TcpCheckHttpMethod.HEAD
32 | export const DEFAULT_DISABLE_WAITING_NETWORK = false
33 | export const DEFAULT_AUTO_CONFIG_KERNEL_PARAMETER = true
34 | export const DEFAULT_TLS_IMPLEMENTATION = TLSImplementation.tls
35 | export const DEFAULT_UTLS_IMITATE = UTLSImitate.chrome_auto
36 | export const DEFAULT_MPTCP = false
37 | export const DEFAULT_ENABLE_LOCAL_TCP_FAST_REDIRECT = false
38 | export const DEFAULT_PPROF_PORT = 0
39 | export const DEFAULT_BANDWIDTH_MAX_TX = '200 mbps'
40 | export const DEFAULT_BANDWIDTH_MAX_RX = '1 gbps'
41 | export const DEFAULT_FALLBACK_RESOLVER = '8.8.8.8:53'
42 |
43 | export const DEFAULT_CONFIG_NAME = 'global'
44 | export const DEFAULT_DNS_NAME = 'default'
45 | export const DEFAULT_ROUTING_NAME = 'default'
46 | export const DEFAULT_GROUP_NAME = 'proxy'
47 |
48 | export const DEFAULT_CONFIG_WITH_LAN_INTERFACEs = (interfaces: string[] = []): GlobalInput => ({
49 | logLevel: DEFAULT_LOG_LEVEL,
50 | tproxyPort: DEFAULT_TPROXY_PORT,
51 | tproxyPortProtect: DEFAULT_TPROXY_PORT_PROTECT,
52 | pprofPort: DEFAULT_PPROF_PORT,
53 | soMarkFromDae: DEFAULT_SO_MARK_FROM_DAE,
54 | allowInsecure: DEFAULT_ALLOW_INSECURE,
55 | checkInterval: `${DEFAULT_CHECK_INTERVAL_SECONDS}s`,
56 | checkTolerance: `${DEFAULT_CHECK_TOLERANCE_MS}ms`,
57 | sniffingTimeout: `${DEFAULT_SNIFFING_TIMEOUT_MS}ms`,
58 | lanInterface: interfaces,
59 | wanInterface: ['auto'],
60 | udpCheckDns: DEFAULT_UDP_CHECK_DNS,
61 | tcpCheckUrl: DEFAULT_TCP_CHECK_URL,
62 | tcpCheckHttpMethod: DEFAULT_TCP_CHECK_HTTP_METHOD,
63 | dialMode: DEFAULT_DIAL_MODE,
64 | autoConfigKernelParameter: DEFAULT_AUTO_CONFIG_KERNEL_PARAMETER,
65 | tlsImplementation: DEFAULT_TLS_IMPLEMENTATION,
66 | utlsImitate: DEFAULT_UTLS_IMITATE,
67 | disableWaitingNetwork: DEFAULT_DISABLE_WAITING_NETWORK,
68 | enableLocalTcpFastRedirect: DEFAULT_ENABLE_LOCAL_TCP_FAST_REDIRECT,
69 | mptcp: DEFAULT_MPTCP,
70 | bandwidthMaxTx: DEFAULT_BANDWIDTH_MAX_TX,
71 | bandwidthMaxRx: DEFAULT_BANDWIDTH_MAX_RX,
72 | fallbackResolver: DEFAULT_FALLBACK_RESOLVER,
73 | })
74 |
75 | export const DEFAULT_GROUP_POLICY = Policy.MinMovingAvg
76 |
77 | export const DEFAULT_ROUTING = `
78 | pname(NetworkManager, systemd-resolved, dnsmasq) -> must_direct
79 | dip(geoip:private) -> direct
80 | dip(geoip:cn) -> direct
81 | domain(geosite:cn) -> direct
82 | fallback: ${DEFAULT_GROUP_NAME}
83 | `.trim()
84 |
85 | export const DEFAULT_DNS = `
86 | upstream {
87 | alidns: 'udp://223.5.5.5:53'
88 | googledns: 'tcp+udp://8.8.8.8:53'
89 | }
90 | routing {
91 | request {
92 | qname(geosite:cn) -> alidns
93 | fallback: googledns
94 | }
95 | }
96 | `.trim()
97 |
98 | export const DEFAULT_V2RAY_FORM_VALUES: z.infer = {
99 | type: 'none',
100 | tls: 'none',
101 | net: 'tcp',
102 | scy: 'auto',
103 | add: '',
104 | aid: 0,
105 | allowInsecure: false,
106 | alpn: '',
107 | flow: 'none',
108 | host: '',
109 | id: '',
110 | path: '',
111 | port: 0,
112 | ps: '',
113 | v: '',
114 | sni: '',
115 | }
116 |
117 | export const DEFAULT_SS_FORM_VALUES: z.infer = {
118 | plugin: '',
119 | method: 'aes-128-gcm',
120 | obfs: 'http',
121 | host: '',
122 | impl: '',
123 | mode: '',
124 | name: '',
125 | password: '',
126 | path: '',
127 | port: 0,
128 | server: '',
129 | tls: '',
130 | }
131 |
132 | export const DEFAULT_SSR_FORM_VALUES: z.infer = {
133 | method: 'aes-128-cfb',
134 | proto: 'origin',
135 | obfs: 'plain',
136 | name: '',
137 | obfsParam: '',
138 | password: '',
139 | port: 0,
140 | protoParam: '',
141 | server: '',
142 | }
143 |
144 | export const DEFAULT_TROJAN_FORM_VALUES: z.infer = {
145 | method: 'origin',
146 | obfs: 'none',
147 | allowInsecure: false,
148 | host: '',
149 | name: '',
150 | password: '',
151 | path: '',
152 | peer: '',
153 | port: 0,
154 | server: '',
155 | ssCipher: 'aes-128-gcm',
156 | ssPassword: '',
157 | }
158 |
159 | export const DEFAULT_TUIC_FORM_VALUES: z.infer = {
160 | name: '',
161 | port: 0,
162 | server: '',
163 | alpn: '',
164 | congestion_control: '',
165 | disable_sni: false,
166 | allowInsecure: false,
167 | uuid: '',
168 | password: '',
169 | udp_relay_mode: '',
170 | sni: '',
171 | }
172 |
173 | export const DEFAULT_JUICITY_FORM_VALUES: z.infer = {
174 | name: '',
175 | port: 0,
176 | server: '',
177 | congestion_control: '',
178 | allowInsecure: false,
179 | uuid: '',
180 | password: '',
181 | pinned_certchain_sha256: '',
182 | sni: '',
183 | }
184 |
185 | export const DEFAULT_HYSTERIA2_FORM_VALUES: z.infer = {
186 | name: '',
187 | port: 443,
188 | server: '',
189 | auth: '',
190 | obfs: '',
191 | obfsPassword: '',
192 | sni: '',
193 | allowInsecure: false,
194 | pinSHA256: '',
195 | }
196 |
197 | export const DEFAULT_HTTP_FORM_VALUES: z.infer = {
198 | host: '',
199 | name: '',
200 | password: '',
201 | port: 0,
202 | username: '',
203 | }
204 |
205 | export const DEFAULT_SOCKS5_FORM_VALUES: z.infer = {
206 | host: '',
207 | name: '',
208 | password: '',
209 | port: 0,
210 | username: '',
211 | }
212 |
--------------------------------------------------------------------------------
/src/constants/editor.ts:
--------------------------------------------------------------------------------
1 | import { EditorProps } from '@monaco-editor/react'
2 | import { languages } from 'monaco-editor'
3 |
4 | export const EDITOR_THEME_DARK = 'vs-dark'
5 | export const EDITOR_THEME_LIGHT = 'githubLight'
6 |
7 | export const EDITOR_OPTIONS: EditorProps['options'] = {
8 | fontSize: 14,
9 | fontWeight: 'bold',
10 | fontFamily: 'Source Code Pro',
11 | 'semanticHighlighting.enabled': true,
12 | lineHeight: 1.6,
13 | minimap: {
14 | enabled: false,
15 | },
16 | scrollBeyondLastLine: false,
17 | renderWhitespace: 'selection',
18 | cursorBlinking: 'solid',
19 | formatOnPaste: true,
20 | insertSpaces: true,
21 | tabSize: 2,
22 | lineNumbers: 'off',
23 | padding: {
24 | top: 8,
25 | bottom: 8,
26 | },
27 | }
28 |
29 | export const EDITOR_LANGUAGE_ROUTINGA: languages.IMonarchLanguage = {
30 | // set defaultToken as `invalid` to turn on debug mode
31 | // defaultToken: 'invalid',
32 | ignoreCase: false,
33 | keywords: [
34 | 'dip',
35 | 'direct',
36 | 'domain',
37 | 'dport',
38 | 'fallback',
39 | 'must_rules',
40 | 'ipversion',
41 | 'l4proto',
42 | 'mac',
43 | 'pname',
44 | 'qname',
45 | 'request',
46 | 'response',
47 | 'routing',
48 | 'sip',
49 | 'sport',
50 | 'tcp',
51 | 'udp',
52 | 'upstream',
53 | ],
54 |
55 | escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
56 |
57 | symbols: /[->&!:,]+/,
58 |
59 | operators: ['&&', '!'],
60 |
61 | tokenizer: {
62 | root: [
63 | [/@[a-zA-Z]\w*/, 'tag'],
64 | [/[a-zA-Z]\w*/, { cases: { '@keywords': 'keyword', '@default': 'identifier' } }],
65 |
66 | { include: '@whitespace' },
67 |
68 | [/[{}()]/, '@brackets'],
69 |
70 | [/@symbols/, { cases: { '@operators': 'operator', '@default': '' } }],
71 |
72 | [/\d+/, 'number'],
73 |
74 | [/[,:]/, 'delimiter'],
75 |
76 | [/"([^"\\]|\\.)*$/, 'string.invalid'],
77 | [/'([^'\\]|\\.)*$/, 'string.invalid'],
78 | [/"/, 'string', '@string_double'],
79 | [/'/, 'string', '@string_single'],
80 | ],
81 |
82 | string_double: [
83 | [/[^\\"]+/, 'string'],
84 | [/@escapes/, 'string.escape'],
85 | [/\\./, 'string.escape.invalid'],
86 | [/"/, 'string', '@pop'],
87 | ],
88 |
89 | string_single: [
90 | [/[^\\']+/, 'string'],
91 | [/@escapes/, 'string.escape'],
92 | [/\\./, 'string.escape.invalid'],
93 | [/'/, 'string', '@pop'],
94 | ],
95 |
96 | whitespace: [
97 | [/[ \t\r\n]+/, 'white'],
98 | [/#.*$/, 'comment'],
99 | ],
100 | },
101 | }
102 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export * from './default'
2 | export * from './editor'
3 | export * from './misc'
4 | export * from './schema'
5 |
--------------------------------------------------------------------------------
/src/constants/misc.ts:
--------------------------------------------------------------------------------
1 | import { TFunction } from 'i18next'
2 |
3 | export enum LogLevel {
4 | error = 'error',
5 | warn = 'warn',
6 | info = 'info',
7 | debug = 'debug',
8 | trace = 'trace',
9 | }
10 |
11 | export enum DialMode {
12 | ip = 'ip',
13 | domain = 'domain',
14 | domainP = 'domain+',
15 | domainPP = 'domain++',
16 | }
17 |
18 | export enum TcpCheckHttpMethod {
19 | CONNECT = 'CONNECT',
20 | HEAD = 'HEAD',
21 | OPTIONS = 'OPTIONS',
22 | TRACE = 'TRACE',
23 | GET = 'GET',
24 | POST = 'POST',
25 | DELETE = 'DELETE',
26 | PATCH = 'PATCH',
27 | PUT = 'PUT',
28 | }
29 |
30 | export enum TLSImplementation {
31 | tls = 'tls',
32 | utls = 'utls',
33 | }
34 |
35 | export enum UTLSImitate {
36 | randomized = 'randomized',
37 | randomizedalpn = 'randomizedalpn',
38 | randomizednoalpn = 'randomizednoalpn',
39 | firefox_auto = 'firefox_auto',
40 | firefox_55 = 'firefox_55',
41 | firefox_56 = 'firefox_56',
42 | firefox_63 = 'firefox_63',
43 | firefox_65 = 'firefox_65',
44 | firefox_99 = 'firefox_99',
45 | firefox_102 = 'firefox_102',
46 | firefox_105 = 'firefox_105',
47 | chrome_auto = 'chrome_auto',
48 | chrome_58 = 'chrome_58',
49 | chrome_62 = 'chrome_62',
50 | chrome_70 = 'chrome_70',
51 | chrome_72 = 'chrome_72',
52 | chrome_83 = 'chrome_83',
53 | chrome_87 = 'chrome_87',
54 | chrome_96 = 'chrome_96',
55 | chrome_100 = 'chrome_100',
56 | chrome_102 = 'chrome_102',
57 | ios_auto = 'ios_auto',
58 | ios_11_1 = 'ios_11_1',
59 | ios_12_1 = 'ios_12_1',
60 | ios_13 = 'ios_13',
61 | ios_14 = 'ios_14',
62 | android_11_okhttp = 'android_11_okhttp',
63 | edge_auto = 'edge_auto',
64 | edge_85 = 'edge_85',
65 | edge_106 = 'edge_106',
66 | safari_auto = 'safari_auto',
67 | safari_16_0 = 'safari_16_0',
68 | utls_360_auto = '360_auto',
69 | utls_360_7_5 = '360_7_5',
70 | utls_360_11_0 = '360_11_0',
71 | qq_auto = 'qq_auto',
72 | qq_11_1 = 'qq_11_1',
73 | }
74 |
75 | export const GET_LOG_LEVEL_STEPS = (t: TFunction) => [
76 | [t('error'), LogLevel.error],
77 | [t('warn'), LogLevel.warn],
78 | [t('info'), LogLevel.info],
79 | [t('debug'), LogLevel.debug],
80 | [t('trace'), LogLevel.trace],
81 | ]
82 |
83 | export enum MODE {
84 | simple = 'simple',
85 | advanced = 'advanced',
86 | }
87 |
88 | export const COLS_PER_ROW = 3
89 | export const QUERY_KEY_HEALTH_CHECK = ['healthCheck']
90 | export const QUERY_KEY_GENERAL = ['general']
91 | export const QUERY_KEY_USER = ['user']
92 | export const QUERY_KEY_NODE = ['node']
93 | export const QUERY_KEY_SUBSCRIPTION = ['subscription']
94 | export const QUERY_KEY_CONFIG = ['config']
95 | export const QUERY_KEY_ROUTING = ['routing']
96 | export const QUERY_KEY_DNS = ['dns']
97 | export const QUERY_KEY_GROUP = ['group']
98 | export const QUERY_KEY_STORAGE = ['storage']
99 |
100 | export enum DraggableResourceType {
101 | node = 'node',
102 | subscription = 'subscription',
103 | subscription_node = 'subscription_node',
104 | groupNode = 'group_node',
105 | groupSubscription = 'group_subscription',
106 | }
107 |
108 | export type DraggingResource = {
109 | type: DraggableResourceType
110 | nodeID?: string
111 | groupID?: string
112 | subscriptionID?: string
113 | }
114 |
115 | export enum RuleType {
116 | config = 'config',
117 | dns = 'dns',
118 | routing = 'routing',
119 | group = 'group',
120 | }
121 |
--------------------------------------------------------------------------------
/src/constants/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const v2raySchema = z.object({
4 | ps: z.string(),
5 | add: z.string().nonempty(),
6 | port: z.number().min(0).max(65535),
7 | id: z.string().nonempty(),
8 | aid: z.number().min(0).max(65535),
9 | net: z.enum(['tcp', 'kcp', 'ws', 'h2', 'grpc']),
10 | type: z.enum(['none', 'http', 'srtp', 'utp', 'wechat-video', 'dtls', 'wireguard']),
11 | host: z.string(),
12 | path: z.string(),
13 | tls: z.enum(['none', 'tls']),
14 | flow: z.enum(['none', 'xtls-rprx-origin', 'xtls-rprx-origin-udp443', 'xtls-rprx-vision', 'xtls-rprx-vision-udp443']),
15 | alpn: z.string(),
16 | scy: z.enum(['auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', 'zero']),
17 | v: z.literal(''),
18 | allowInsecure: z.boolean(),
19 | sni: z.string(),
20 | })
21 |
22 | export const ssSchema = z.object({
23 | method: z.enum(['aes-128-gcm', 'aes-256-gcm', 'chacha20-poly1305', 'chacha20-ietf-poly1305', 'plain', 'none']),
24 | plugin: z.enum(['', 'simple-obfs', 'v2ray-plugin']),
25 | obfs: z.enum(['http', 'tls']),
26 | tls: z.enum(['', 'tls']),
27 | path: z.string(),
28 | mode: z.string(),
29 | host: z.string(),
30 | password: z.string().nonempty(),
31 | server: z.string().nonempty(),
32 | port: z.number().min(0).max(65535),
33 | name: z.string(),
34 | impl: z.enum(['', 'chained', 'transport']),
35 | })
36 |
37 | export const ssrSchema = z.object({
38 | method: z.enum([
39 | 'aes-128-cfb',
40 | 'aes-192-cfb',
41 | 'aes-256-cfb',
42 | 'aes-128-ctr',
43 | 'aes-192-ctr',
44 | 'aes-256-ctr',
45 | 'aes-128-ofb',
46 | 'aes-192-ofb',
47 | 'aes-256-ofb',
48 | 'des-cfb',
49 | 'bf-cfb',
50 | 'cast5-cfb',
51 | 'rc4-md5',
52 | 'chacha20-ietf',
53 | 'salsa20',
54 | 'camellia-128-cfb',
55 | 'camellia-192-cfb',
56 | 'camellia-256-cfb',
57 | 'idea-cfb',
58 | 'rc2-cfb',
59 | 'seed-cfb',
60 | 'none',
61 | ]),
62 | password: z.string().nonempty(),
63 | server: z.string().nonempty(),
64 | port: z.number().min(0).max(65535).positive(),
65 | name: z.string(),
66 | proto: z.enum([
67 | 'origin',
68 | 'verify_sha1',
69 | 'auth_sha1_v4',
70 | 'auth_aes128_md5',
71 | 'auth_aes128_sha1',
72 | 'auth_chain_a',
73 | 'auth_chain_b',
74 | ]),
75 | protoParam: z.string(),
76 | obfs: z.enum(['plain', 'http_simple', 'http_post', 'random_head', 'tls1.2_ticket_auth']),
77 | obfsParam: z.string(),
78 | })
79 |
80 | export const trojanSchema = z.object({
81 | name: z.string(),
82 | server: z.string().nonempty(),
83 | peer: z.string(),
84 | host: z.string(),
85 | path: z.string(),
86 | allowInsecure: z.boolean(),
87 | port: z.number().min(0).max(65535),
88 | password: z.string().nonempty(),
89 | method: z.enum(['origin', 'shadowsocks']),
90 | ssCipher: z.enum(['aes-128-gcm', 'aes-256-gcm', 'chacha20-poly1305', 'chacha20-ietf-poly1305']),
91 | ssPassword: z.string(),
92 | obfs: z.enum(['none', 'websocket']),
93 | })
94 |
95 | export const tuicSchema = z.object({
96 | name: z.string(),
97 | server: z.string().nonempty(),
98 | port: z.number().min(0).max(65535),
99 | uuid: z.string().nonempty(),
100 | password: z.string().nonempty(),
101 | allowInsecure: z.boolean(),
102 | disable_sni: z.boolean(),
103 | sni: z.string(),
104 | congestion_control: z.string(),
105 | alpn: z.string(),
106 | udp_relay_mode: z.string(),
107 | })
108 |
109 | export const juicitySchema = z.object({
110 | name: z.string(),
111 | server: z.string().nonempty(),
112 | port: z.number().min(0).max(65535),
113 | uuid: z.string().nonempty(),
114 | password: z.string().nonempty(),
115 | allowInsecure: z.boolean(),
116 | pinned_certchain_sha256: z.string(),
117 | sni: z.string(),
118 | congestion_control: z.string(),
119 | })
120 |
121 | export const hysteria2Schema = z.object({
122 | name: z.string(),
123 | server: z.string().nonempty(),
124 | port: z.number().min(0).max(65535),
125 | auth: z.string(),
126 | obfs: z.string(),
127 | obfsPassword: z.string(),
128 | sni: z.string(),
129 | allowInsecure: z.boolean(),
130 | pinSHA256: z.string(),
131 | })
132 |
133 | export const httpSchema = z.object({
134 | username: z.string(),
135 | password: z.string(),
136 | host: z.string().nonempty(),
137 | port: z.number().min(0).max(65535),
138 | name: z.string(),
139 | })
140 |
141 | export const socks5Schema = z.object({
142 | username: z.string(),
143 | password: z.string(),
144 | host: z.string().nonempty(),
145 | port: z.number().min(0).max(65535),
146 | name: z.string(),
147 | })
148 |
--------------------------------------------------------------------------------
/src/contexts/index.tsx:
--------------------------------------------------------------------------------
1 | import { notifications } from '@mantine/notifications'
2 | import { useStore } from '@nanostores/react'
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4 | import { ClientError, GraphQLClient } from 'graphql-request'
5 | import { createContext, useContext, useMemo } from 'react'
6 |
7 | import { endpointURLAtom, tokenAtom } from '~/store'
8 |
9 | export const GQLClientContext = createContext(null as unknown as GraphQLClient)
10 |
11 | export const GQLQueryClientProvider = ({ client, children }: { client: GraphQLClient; children: React.ReactNode }) => {
12 | return {children}
13 | }
14 |
15 | export const useGQLQueryClient = () => useContext(GQLClientContext)
16 |
17 | export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
18 | const endpointURL = useStore(endpointURLAtom)
19 | const token = useStore(tokenAtom)
20 |
21 | const queryClient = useMemo(() => new QueryClient(), [])
22 |
23 | const gqlClient = useMemo(
24 | () =>
25 | new GraphQLClient(endpointURL, {
26 | headers: {
27 | authorization: `Bearer ${token}`,
28 | },
29 | responseMiddleware: (response) => {
30 | const error = (response as ClientError).response?.errors?.[0]
31 |
32 | if (error) {
33 | notifications.show({
34 | color: 'red',
35 | message: error.message,
36 | })
37 |
38 | if (error.message === 'access denied') {
39 | tokenAtom.set('')
40 | }
41 | }
42 |
43 | return response
44 | },
45 | }),
46 | [endpointURL, token],
47 | )
48 |
49 | return (
50 |
51 | {children}
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next'
2 | import detectLanguage from 'i18next-browser-languagedetector'
3 | import { initReactI18next } from 'react-i18next'
4 |
5 | import en from './locales/en.json'
6 | import zhHans from './locales/zh-Hans.json'
7 |
8 | export const defaultNS = 'translation'
9 |
10 | export const resources = {
11 | en: { [defaultNS]: en },
12 | 'zh-Hans': { [defaultNS]: zhHans },
13 | }
14 |
15 | const i18nInit = () =>
16 | i18n
17 | .use(detectLanguage)
18 | .use(initReactI18next)
19 | .init({
20 | fallbackLng: {
21 | 'zh-CN': ['zh-Hans'],
22 | },
23 | defaultNS,
24 | resources,
25 | })
26 |
27 | export { i18n, i18nInit }
28 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import '@fontsource/fira-sans';
2 | @import '@fontsource/source-code-pro';
3 | @import 'graphiql/graphiql.css';
4 |
5 | #root {
6 | display: contents;
7 | }
8 |
9 | ::selection {
10 | color: whitesmoke;
11 | background-color: goldenrod;
12 | }
13 |
14 | form {
15 | display: contents;
16 | }
17 |
--------------------------------------------------------------------------------
/src/initialize.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import {
4 | getDefaultsRequest,
5 | getInterfacesRequest,
6 | getModeRequest,
7 | useCreateConfigMutation,
8 | useCreateDNSMutation,
9 | useCreateGroupMutation,
10 | useCreateRoutingMutation,
11 | useSelectConfigMutation,
12 | useSelectDNSMutation,
13 | useSelectRoutingMutation,
14 | useSetJsonStorageMutation,
15 | } from '~/apis'
16 | import {
17 | DEFAULT_CONFIG_NAME,
18 | DEFAULT_CONFIG_WITH_LAN_INTERFACEs,
19 | DEFAULT_DNS,
20 | DEFAULT_DNS_NAME,
21 | DEFAULT_GROUP_NAME,
22 | DEFAULT_GROUP_POLICY,
23 | DEFAULT_ROUTING,
24 | DEFAULT_ROUTING_NAME,
25 | MODE,
26 | } from '~/constants'
27 | import { useGQLQueryClient } from '~/contexts'
28 | import { defaultResourcesAtom, modeAtom } from '~/store'
29 |
30 | export const useInitialize = () => {
31 | const createConfigMutation = useCreateConfigMutation()
32 | const selectConfigMutation = useSelectConfigMutation()
33 | const createRoutingMutation = useCreateRoutingMutation()
34 | const selectRoutingMutation = useSelectRoutingMutation()
35 | const createDNSMutation = useCreateDNSMutation()
36 | const selectDNSMutation = useSelectDNSMutation()
37 | const createGroupMutation = useCreateGroupMutation()
38 | const setJsonStorageMutation = useSetJsonStorageMutation()
39 | const gqlClient = useGQLQueryClient()
40 | const getInterfaces = getInterfacesRequest(gqlClient)
41 | const getMode = getModeRequest(gqlClient)
42 | const getDefaults = getDefaultsRequest(gqlClient)
43 |
44 | return useCallback(async () => {
45 | const lanInterfaces = (await getInterfaces()).general.interfaces
46 | .filter(({ flag }) => !!flag.default)
47 | .map(({ name }) => name)
48 |
49 | const { defaultConfigID, defaultRoutingID, defaultDNSID, defaultGroupID } = await getDefaults()
50 |
51 | if (!defaultConfigID) {
52 | const {
53 | createConfig: { id },
54 | } = await createConfigMutation.mutateAsync({
55 | name: DEFAULT_CONFIG_NAME,
56 | global: DEFAULT_CONFIG_WITH_LAN_INTERFACEs(lanInterfaces),
57 | })
58 |
59 | await selectConfigMutation.mutateAsync({ id })
60 | await setJsonStorageMutation.mutateAsync({ defaultConfigID: id })
61 | }
62 |
63 | if (!defaultRoutingID) {
64 | const {
65 | createRouting: { id },
66 | } = await createRoutingMutation.mutateAsync({ name: DEFAULT_ROUTING_NAME, routing: DEFAULT_ROUTING })
67 |
68 | await selectRoutingMutation.mutateAsync({ id })
69 | await setJsonStorageMutation.mutateAsync({ defaultRoutingID: id })
70 | }
71 |
72 | if (!defaultDNSID) {
73 | const {
74 | createDns: { id },
75 | } = await createDNSMutation.mutateAsync({ name: DEFAULT_DNS_NAME, dns: DEFAULT_DNS })
76 |
77 | await selectDNSMutation.mutateAsync({ id })
78 | await setJsonStorageMutation.mutateAsync({ defaultDNSID: id })
79 | }
80 |
81 | if (!defaultGroupID) {
82 | const {
83 | createGroup: { id },
84 | } = await createGroupMutation.mutateAsync({
85 | name: DEFAULT_GROUP_NAME,
86 | policy: DEFAULT_GROUP_POLICY,
87 | policyParams: [],
88 | })
89 | await setJsonStorageMutation.mutateAsync({ defaultGroupID: id })
90 | }
91 |
92 | const mode = await getMode()
93 |
94 | if (!mode) {
95 | await setJsonStorageMutation.mutateAsync({ mode: MODE.simple })
96 |
97 | modeAtom.set(MODE.simple)
98 | } else {
99 | modeAtom.set(mode as MODE)
100 | }
101 |
102 | {
103 | const { defaultConfigID, defaultDNSID, defaultGroupID, defaultRoutingID } = await getDefaults()
104 |
105 | defaultResourcesAtom.set({ defaultConfigID, defaultDNSID, defaultGroupID, defaultRoutingID })
106 | }
107 | }, [
108 | createConfigMutation,
109 | createDNSMutation,
110 | createGroupMutation,
111 | createRoutingMutation,
112 | getDefaults,
113 | getInterfaces,
114 | getMode,
115 | selectConfigMutation,
116 | selectDNSMutation,
117 | selectRoutingMutation,
118 | setJsonStorageMutation,
119 | ])
120 | }
121 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/default */
2 |
3 | import '~/index.css'
4 |
5 | import { loader } from '@monaco-editor/react'
6 | import dayjs from 'dayjs'
7 | import duration from 'dayjs/plugin/duration'
8 | import * as monaco from 'monaco-editor'
9 | import { editor } from 'monaco-editor'
10 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
11 | import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
12 | import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
13 | import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
14 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
15 | import ReactDOM from 'react-dom/client'
16 |
17 | import { App } from '~/App'
18 | import { EDITOR_LANGUAGE_ROUTINGA } from '~/constants/editor'
19 | import { i18nInit } from '~/i18n'
20 |
21 | dayjs.extend(duration)
22 |
23 | const loadMonaco = async () => {
24 | loader.config({ monaco })
25 |
26 | self.MonacoEnvironment = {
27 | createTrustedTypesPolicy() {
28 | return undefined
29 | },
30 |
31 | getWorker(_, label) {
32 | if (label === 'json') {
33 | return new jsonWorker()
34 | }
35 |
36 | if (label === 'css' || label === 'scss' || label === 'less') {
37 | return new cssWorker()
38 | }
39 |
40 | if (label === 'html' || label === 'handlebars' || label === 'razor') {
41 | return new htmlWorker()
42 | }
43 |
44 | if (label === 'typescript' || label === 'javascript') {
45 | return new tsWorker()
46 | }
47 |
48 | return new editorWorker()
49 | },
50 | }
51 |
52 | const monacoInstance = await loader.init()
53 |
54 | monacoInstance.languages.register({ id: 'routingA', extensions: ['dae'] })
55 | monacoInstance.languages.setMonarchTokensProvider('routingA', EDITOR_LANGUAGE_ROUTINGA)
56 |
57 | const themeGithub = await import('monaco-themes/themes/GitHub.json')
58 | const themeGithubLight = await import('monaco-themes/themes/GitHub Light.json')
59 | monacoInstance.editor.defineTheme('github', themeGithub as editor.IStandaloneThemeData)
60 | monacoInstance.editor.defineTheme('githubLight', themeGithubLight as editor.IStandaloneThemeData)
61 | }
62 |
63 | Promise.all([i18nInit(), loadMonaco()]).then(() => {
64 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render()
65 | })
66 |
--------------------------------------------------------------------------------
/src/pages/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Anchor, AppShell, Center, Container, Footer, Text } from '@mantine/core'
2 | import { useStore } from '@nanostores/react'
3 | import { useEffect } from 'react'
4 | import { Outlet, useNavigate } from 'react-router-dom'
5 |
6 | import { HeaderWithActions } from '~/components/Header'
7 | import { useInitialize } from '~/initialize'
8 | import { endpointURLAtom, tokenAtom } from '~/store'
9 |
10 | export const MainLayout = () => {
11 | const navigate = useNavigate()
12 | const token = useStore(tokenAtom)
13 | const endpointURL = useStore(endpointURLAtom)
14 | const initialize = useInitialize()
15 |
16 | useEffect(() => {
17 | initialize()
18 | // eslint-disable-next-line react-hooks/exhaustive-deps
19 | }, [])
20 |
21 | useEffect(() => {
22 | if (!endpointURL || !token) {
23 | navigate('/setup')
24 | }
25 | }, [endpointURL, navigate, token])
26 |
27 | return (
28 | }
30 | footer={
31 |
41 | }
42 | >
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/src/pages/Orchestrate/Config.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { Prism } from '@mantine/prism'
4 | import { useStore } from '@nanostores/react'
5 | import { IconEdit, IconForms, IconSettings } from '@tabler/icons-react'
6 | import { Fragment, useRef } from 'react'
7 | import { useTranslation } from 'react-i18next'
8 |
9 | import { useConfigsQuery, useRemoveConfigMutation, useSelectConfigMutation } from '~/apis'
10 | import { ConfigFormDrawer, ConfigFormModalRef } from '~/components/ConfigFormModal'
11 | import { RenameFormModal, RenameFormModalRef } from '~/components/RenameFormModal'
12 | import { Section } from '~/components/Section'
13 | import { SimpleCard } from '~/components/SimpleCard'
14 | import { GET_LOG_LEVEL_STEPS, RuleType } from '~/constants'
15 | import { defaultResourcesAtom } from '~/store'
16 | import { deriveTime } from '~/utils'
17 |
18 | export const Config = () => {
19 | const { t } = useTranslation()
20 |
21 | const { defaultConfigID } = useStore(defaultResourcesAtom)
22 |
23 | const { data: configsQuery } = useConfigsQuery()
24 | const selectConfigMutation = useSelectConfigMutation()
25 | const removeConfigMutation = useRemoveConfigMutation()
26 | const updateConfigFormModalRef = useRef(null)
27 |
28 | const [openedRenameFormModal, { open: openRenameFormModal, close: closeRenameFormModal }] = useDisclosure(false)
29 | const renameFormModalRef = useRef(null)
30 |
31 | const [openedCreateConfigFormDrawer, { open: openCreateConfigFormDrawer, close: closeCreateConfigFormDrawer }] =
32 | useDisclosure(false)
33 | const [openedUpdateConfigFormDrawer, { open: openUpdateConfigFormDrawer, close: closeUpdateConfigFormDrawer }] =
34 | useDisclosure(false)
35 |
36 | return (
37 | } onCreate={openCreateConfigFormDrawer} bordered>
38 | {configsQuery?.configs.map((config) => (
39 |
44 | {
47 | if (renameFormModalRef.current) {
48 | renameFormModalRef.current.setProps({
49 | id: config.id,
50 | type: RuleType.config,
51 | oldName: config.name,
52 | })
53 | }
54 |
55 | openRenameFormModal()
56 | }}
57 | >
58 |
59 |
60 |
61 | {
64 | updateConfigFormModalRef.current?.setEditingID(config.id)
65 |
66 | const { checkInterval, checkTolerance, sniffingTimeout, logLevel, ...global } = config.global
67 |
68 | const logLevelSteps = GET_LOG_LEVEL_STEPS(t)
69 | const logLevelNumber = logLevelSteps.findIndex(([, l]) => l === logLevel)
70 |
71 | updateConfigFormModalRef.current?.initOrigins({
72 | name: config.name,
73 | logLevelNumber,
74 | checkIntervalSeconds: deriveTime(checkInterval, 's'),
75 | checkToleranceMS: deriveTime(checkTolerance, 'ms'),
76 | sniffingTimeoutMS: deriveTime(sniffingTimeout, 'ms'),
77 | ...global,
78 | })
79 |
80 | openUpdateConfigFormDrawer()
81 | }}
82 | >
83 |
84 |
85 |
86 | }
87 | selected={config.selected}
88 | onSelect={() => selectConfigMutation.mutate({ id: config.id })}
89 | onRemove={config.id !== defaultConfigID ? () => removeConfigMutation.mutate(config.id) : undefined}
90 | >
91 | {JSON.stringify(config, null, 2)}
92 |
93 | ))}
94 |
95 |
96 |
101 |
102 |
103 |
104 | )
105 | }
106 |
--------------------------------------------------------------------------------
/src/pages/Orchestrate/DNS.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { Prism } from '@mantine/prism'
4 | import { useStore } from '@nanostores/react'
5 | import { IconEdit, IconForms, IconRoute } from '@tabler/icons-react'
6 | import { Fragment, useRef } from 'react'
7 | import { useTranslation } from 'react-i18next'
8 |
9 | import {
10 | useCreateDNSMutation,
11 | useDNSsQuery,
12 | useRemoveDNSMutation,
13 | useSelectDNSMutation,
14 | useUpdateDNSMutation,
15 | } from '~/apis'
16 | import { PlainTextFormModal, PlainTextgFormModalRef } from '~/components/PlainTextFormModal'
17 | import { RenameFormModal, RenameFormModalRef } from '~/components/RenameFormModal'
18 | import { Section } from '~/components/Section'
19 | import { SimpleCard } from '~/components/SimpleCard'
20 | import { RuleType } from '~/constants'
21 | import { defaultResourcesAtom } from '~/store'
22 |
23 | export const DNS = () => {
24 | const { t } = useTranslation()
25 |
26 | const { defaultDNSID } = useStore(defaultResourcesAtom)
27 | const { data: dnssQuery } = useDNSsQuery()
28 | const selectDNSMutation = useSelectDNSMutation()
29 | const removeDNSMutation = useRemoveDNSMutation()
30 | const createDNSMutation = useCreateDNSMutation()
31 | const updateDNSFormModalRef = useRef(null)
32 | const updateDNSMutation = useUpdateDNSMutation()
33 |
34 | const renameFormModalRef = useRef(null)
35 | const [openedRenameFormModal, { open: openRenameFormModal, close: closeRenameFormModal }] = useDisclosure(false)
36 | const [openedCreateDNSFormModal, { open: openCreateDNSFormModal, close: closeCreateDNSFormModal }] =
37 | useDisclosure(false)
38 | const [openedUpdateDNSFormModal, { open: openUpdateDNSFormModal, close: closeUpdateDNSFormModal }] =
39 | useDisclosure(false)
40 |
41 | return (
42 | } onCreate={openCreateDNSFormModal} bordered>
43 | {dnssQuery?.dnss.map((dns) => (
44 |
49 | {
52 | if (renameFormModalRef.current) {
53 | renameFormModalRef.current.setProps({
54 | id: dns.id,
55 | type: RuleType.dns,
56 | oldName: dns.name,
57 | })
58 | }
59 |
60 | openRenameFormModal()
61 | }}
62 | >
63 |
64 |
65 |
66 | {
69 | updateDNSFormModalRef.current?.setEditingID(dns.id)
70 |
71 | updateDNSFormModalRef.current?.initOrigins({
72 | name: dns.name,
73 | text: dns.dns.string,
74 | })
75 |
76 | openUpdateDNSFormModal()
77 | }}
78 | >
79 |
80 |
81 |
82 | }
83 | selected={dns.selected}
84 | onSelect={() => selectDNSMutation.mutate({ id: dns.id })}
85 | onRemove={dns.id !== defaultDNSID ? () => removeDNSMutation.mutate(dns.id) : undefined}
86 | >
87 | {dns.dns.string}
88 |
89 | ))}
90 |
91 | {
96 | await createDNSMutation.mutateAsync({
97 | name: values.name,
98 | dns: values.text,
99 | })
100 | }}
101 | />
102 |
103 | {
109 | if (updateDNSFormModalRef.current) {
110 | await updateDNSMutation.mutateAsync({
111 | id: updateDNSFormModalRef.current.editingID,
112 | dns: values.text,
113 | })
114 | }
115 | }}
116 | />
117 |
118 |
119 |
120 | )
121 | }
122 |
--------------------------------------------------------------------------------
/src/pages/Orchestrate/Group.tsx:
--------------------------------------------------------------------------------
1 | import { Accordion, ActionIcon, SimpleGrid, Space, Text } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { useStore } from '@nanostores/react'
4 | import { IconEdit, IconForms, IconTable } from '@tabler/icons-react'
5 | import { Fragment, useRef, useState } from 'react'
6 | import { useTranslation } from 'react-i18next'
7 |
8 | import {
9 | useGroupDelNodesMutation,
10 | useGroupDelSubscriptionsMutation,
11 | useGroupsQuery,
12 | useRemoveGroupMutation,
13 | useSubscriptionsQuery,
14 | } from '~/apis'
15 | import { DraggableResourceBadge } from '~/components/DraggableResourceBadge'
16 | import { DroppableGroupCard } from '~/components/DroppableGroupCard'
17 | import { GroupFormModal, GroupFormModalRef } from '~/components/GroupFormModal'
18 | import { RenameFormModal, RenameFormModalRef } from '~/components/RenameFormModal'
19 | import { Section } from '~/components/Section'
20 | import { DraggableResourceType, RuleType } from '~/constants'
21 | import { defaultResourcesAtom } from '~/store'
22 |
23 | export const GroupResource = ({ highlight }: { highlight?: boolean }) => {
24 | const { t } = useTranslation()
25 | const { data: groupsQuery } = useGroupsQuery()
26 | const { defaultGroupID } = useStore(defaultResourcesAtom)
27 | const [openedRenameFormModal, { open: openRenameFormModal, close: closeRenameFormModal }] = useDisclosure(false)
28 | const [openedCreateGroupFormModal, { open: openCreateGroupFormModal, close: closeCreateGroupFormModal }] =
29 | useDisclosure(false)
30 | const [openedUpdateGroupFormModal, { open: openUpdateGroupFormModal, close: closeUpdateGroupFormModal }] =
31 | useDisclosure(false)
32 | const removeGroupMutation = useRemoveGroupMutation()
33 | const groupDelNodesMutation = useGroupDelNodesMutation()
34 | const groupDelSubscriptionsMutation = useGroupDelSubscriptionsMutation()
35 | const [droppableGroupCardAccordionValues, setDroppableGroupCardAccordionValues] = useState([])
36 | const renameFormModalRef = useRef(null)
37 | const updateGroupFormModalRef = useRef(null)
38 | const { data: subscriptionsQuery } = useSubscriptionsQuery()
39 |
40 | return (
41 | } onCreate={openCreateGroupFormModal} highlight={highlight} bordered>
42 | {groupsQuery?.groups.map(
43 | ({ id: groupId, name, policy, nodes: groupNodes, subscriptions: groupSubscriptions }) => (
44 | removeGroupMutation.mutate(groupId) : undefined}
49 | actions={
50 |
51 | {
54 | if (renameFormModalRef.current) {
55 | renameFormModalRef.current.setProps({
56 | id: groupId,
57 | type: RuleType.group,
58 | oldName: name,
59 | })
60 | }
61 |
62 | openRenameFormModal()
63 | }}
64 | >
65 |
66 |
67 |
68 | {
71 | updateGroupFormModalRef.current?.setEditingID(groupId)
72 |
73 | updateGroupFormModalRef.current?.initOrigins({
74 | name,
75 | policy,
76 | })
77 |
78 | openUpdateGroupFormModal()
79 | }}
80 | >
81 |
82 |
83 |
84 | }
85 | >
86 |
87 | {policy}
88 |
89 |
90 |
91 |
92 |
98 | {groupNodes.length > 0 && (
99 |
100 |
101 | {t('node')} ({groupNodes.length})
102 |
103 |
104 |
105 |
106 | {groupNodes.map(({ id: nodeId, tag, name, subscriptionID }) => (
107 |
115 | groupDelNodesMutation.mutate({
116 | id: groupId,
117 | nodeIDs: [nodeId],
118 | })
119 | }
120 | >
121 | {subscriptionID &&
122 | subscriptionsQuery?.subscriptions.find((s) => s.id === subscriptionID)?.tag}
123 |
124 | ))}
125 |
126 |
127 |
128 | )}
129 |
130 | {groupSubscriptions.length > 0 && (
131 |
132 |
133 | {t('subscription')} ({groupSubscriptions.length})
134 |
135 |
136 |
137 |
138 | {groupSubscriptions.map(({ id: subscriptionId, tag, link }) => (
139 |
147 | groupDelSubscriptionsMutation.mutate({
148 | id: groupId,
149 | subscriptionIDs: [subscriptionId],
150 | })
151 | }
152 | />
153 | ))}
154 |
155 |
156 |
157 | )}
158 |
159 |
160 | ),
161 | )}
162 |
163 |
164 |
169 |
170 |
171 |
172 | )
173 | }
174 |
--------------------------------------------------------------------------------
/src/pages/Orchestrate/Node.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon, Spoiler, Text, useMantineTheme } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { IconCloud, IconCloudPlus, IconEye, IconFileImport } from '@tabler/icons-react'
4 | import { useRef } from 'react'
5 | import { useTranslation } from 'react-i18next'
6 |
7 | import { useImportNodesMutation, useNodesQuery, useRemoveNodesMutation } from '~/apis'
8 | import { ConfigureNodeFormModal } from '~/components'
9 | import { DraggableResourceCard } from '~/components/DraggableResourceCard'
10 | import { ImportResourceFormModal } from '~/components/ImportResourceFormModal'
11 | import { QRCodeModal, QRCodeModalRef } from '~/components/QRCodeModal'
12 | import { Section } from '~/components/Section'
13 | import { DraggableResourceType } from '~/constants'
14 |
15 | export const NodeResource = () => {
16 | const { t } = useTranslation()
17 | const theme = useMantineTheme()
18 |
19 | const [openedQRCodeModal, { open: openQRCodeModal, close: closeQRCodeModal }] = useDisclosure(false)
20 | const [openedImportNodeFormModal, { open: openImportNodeFormModal, close: closeImportNodeFormModal }] =
21 | useDisclosure(false)
22 | const [openedConfigureNodeFormModal, { open: openConfigureNodeFormModal, close: closeConfigureNodeFormModal }] =
23 | useDisclosure(false)
24 | const qrCodeModalRef = useRef(null)
25 | const { data: nodesQuery } = useNodesQuery()
26 | const removeNodesMutation = useRemoveNodesMutation()
27 | const importNodesMutation = useImportNodesMutation()
28 |
29 | return (
30 | }
33 | iconPlus={}
34 | onCreate={openImportNodeFormModal}
35 | actions={
36 |
37 |
38 |
39 | }
40 | bordered
41 | >
42 | {nodesQuery?.nodes.edges.map(({ id, name, tag, protocol, link }) => (
43 |
51 | {protocol}
52 |
53 | }
54 | actions={
55 | {
58 | qrCodeModalRef.current?.setProps({
59 | name: name || tag!,
60 | link,
61 | })
62 | openQRCodeModal()
63 | }}
64 | >
65 |
66 |
67 | }
68 | onRemove={() => removeNodesMutation.mutate([id])}
69 | >
70 |
71 | {name}
72 |
73 |
74 | {t('actions.show content')}}
77 | hideLabel={{t('actions.hide')}}
78 | >
79 |
85 | {link}
86 |
87 |
88 |
89 | ))}
90 |
91 |
92 |
93 | {
98 | await importNodesMutation.mutateAsync(values.resources.map(({ link, tag }) => ({ link, tag })))
99 | }}
100 | />
101 |
102 |
103 |
104 | )
105 | }
106 |
--------------------------------------------------------------------------------
/src/pages/Orchestrate/Routing.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { Prism } from '@mantine/prism'
4 | import { useStore } from '@nanostores/react'
5 | import { IconEdit, IconForms, IconMap } from '@tabler/icons-react'
6 | import { Fragment, useRef } from 'react'
7 | import { useTranslation } from 'react-i18next'
8 |
9 | import {
10 | useCreateRoutingMutation,
11 | useRemoveRoutingMutation,
12 | useRoutingsQuery,
13 | useSelectRoutingMutation,
14 | useUpdateRoutingMutation,
15 | } from '~/apis'
16 | import { PlainTextFormModal, PlainTextgFormModalRef } from '~/components/PlainTextFormModal'
17 | import { RenameFormModal, RenameFormModalRef } from '~/components/RenameFormModal'
18 | import { Section } from '~/components/Section'
19 | import { SimpleCard } from '~/components/SimpleCard'
20 | import { RuleType } from '~/constants'
21 | import { defaultResourcesAtom } from '~/store'
22 |
23 | export const Routing = () => {
24 | const { t } = useTranslation()
25 | const { defaultRoutingID } = useStore(defaultResourcesAtom)
26 | const { data: routingsQuery } = useRoutingsQuery()
27 | const selectRoutingMutation = useSelectRoutingMutation()
28 | const removeRoutingMutation = useRemoveRoutingMutation()
29 | const createRoutingMutation = useCreateRoutingMutation()
30 | const updateRoutingFormModalRef = useRef(null)
31 | const updateRoutingMutation = useUpdateRoutingMutation()
32 |
33 | const renameFormModalRef = useRef(null)
34 | const [openedRenameFormModal, { open: openRenameFormModal, close: closeRenameFormModal }] = useDisclosure(false)
35 | const [openedCreateRoutingFormModal, { open: openCreateRoutingFormModal, close: closeCreateRoutingFormModal }] =
36 | useDisclosure(false)
37 | const [openedUpdateRoutingFormModal, { open: openUpdateRoutingFormModal, close: closeUpdateRoutingFormModal }] =
38 | useDisclosure(false)
39 |
40 | return (
41 | } onCreate={openCreateRoutingFormModal} bordered>
42 | {routingsQuery?.routings.map((routing) => (
43 |
48 | {
51 | if (renameFormModalRef.current) {
52 | renameFormModalRef.current.setProps({
53 | id: routing.id,
54 | type: RuleType.routing,
55 | oldName: routing.name,
56 | })
57 | }
58 |
59 | openRenameFormModal()
60 | }}
61 | >
62 |
63 |
64 |
65 | {
68 | updateRoutingFormModalRef.current?.setEditingID(routing.id)
69 |
70 | updateRoutingFormModalRef.current?.initOrigins({
71 | name: routing.name,
72 | text: routing.routing.string,
73 | })
74 |
75 | openUpdateRoutingFormModal()
76 | }}
77 | >
78 |
79 |
80 |
81 | }
82 | selected={routing.selected}
83 | onSelect={() => selectRoutingMutation.mutate({ id: routing.id })}
84 | onRemove={routing.id !== defaultRoutingID ? () => removeRoutingMutation.mutate(routing.id) : undefined}
85 | >
86 | {routing.routing.string}
87 |
88 | ))}
89 |
90 | {
95 | await createRoutingMutation.mutateAsync({
96 | name: values.name,
97 | routing: values.text,
98 | })
99 | }}
100 | />
101 |
102 | {
108 | if (updateRoutingFormModalRef.current) {
109 | await updateRoutingMutation.mutateAsync({
110 | id: updateRoutingFormModalRef.current.editingID,
111 | routing: values.text,
112 | })
113 | }
114 | }}
115 | />
116 |
117 |
118 |
119 | )
120 | }
121 |
--------------------------------------------------------------------------------
/src/pages/Orchestrate/Subscription.tsx:
--------------------------------------------------------------------------------
1 | import { Accordion, ActionIcon, Group, Spoiler, Text } from '@mantine/core'
2 | import { useDisclosure } from '@mantine/hooks'
3 | import { IconCloudComputing, IconCloudPlus, IconDownload, IconEye } from '@tabler/icons-react'
4 | import dayjs from 'dayjs'
5 | import { Fragment, useRef } from 'react'
6 | import { useTranslation } from 'react-i18next'
7 |
8 | import {
9 | useImportSubscriptionsMutation,
10 | useRemoveSubscriptionsMutation,
11 | useSubscriptionsQuery,
12 | useUpdateSubscriptionsMutation,
13 | } from '~/apis'
14 | import { DraggableResourceBadge } from '~/components/DraggableResourceBadge'
15 | import { DraggableResourceCard } from '~/components/DraggableResourceCard'
16 | import { ImportResourceFormModal } from '~/components/ImportResourceFormModal'
17 | import { QRCodeModal, QRCodeModalRef } from '~/components/QRCodeModal'
18 | import { Section } from '~/components/Section'
19 | import { UpdateSubscriptionAction } from '~/components/UpdateSubscriptionAction'
20 | import { DraggableResourceType } from '~/constants'
21 |
22 | export const SubscriptionResource = () => {
23 | const { t } = useTranslation()
24 |
25 | const [openedQRCodeModal, { open: openQRCodeModal, close: closeQRCodeModal }] = useDisclosure(false)
26 | const [
27 | openedImportSubscriptionFormModal,
28 | { open: openImportSubscriptionFormModal, close: closeImportSubscriptionFormModal },
29 | ] = useDisclosure(false)
30 | const qrCodeModalRef = useRef(null)
31 | const { data: subscriptionsQuery } = useSubscriptionsQuery()
32 | const removeSubscriptionsMutation = useRemoveSubscriptionsMutation()
33 | const importSubscriptionsMutation = useImportSubscriptionsMutation()
34 | const updateSubscriptionsMutation = useUpdateSubscriptionsMutation()
35 |
36 | return (
37 | }
40 | iconPlus={}
41 | onCreate={openImportSubscriptionFormModal}
42 | bordered
43 | actions={
44 | subscriptionsQuery?.subscriptions &&
45 | subscriptionsQuery.subscriptions.length > 2 && (
46 | {
48 | updateSubscriptionsMutation.mutate(subscriptionsQuery?.subscriptions.map(({ id }) => id) || [])
49 | }}
50 | loading={updateSubscriptionsMutation.isLoading}
51 | >
52 |
53 |
54 | )
55 | }
56 | >
57 | {subscriptionsQuery?.subscriptions.map(({ id: subscriptionID, tag, link, updatedAt, nodes }) => (
58 |
66 | {
69 | qrCodeModalRef.current?.setProps({
70 | name: tag!,
71 | link,
72 | })
73 | openQRCodeModal()
74 | }}
75 | >
76 |
77 |
78 |
79 |
80 | }
81 | onRemove={() => removeSubscriptionsMutation.mutate([subscriptionID])}
82 | >
83 | {dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}
84 |
85 | {t('actions.show content')}}
88 | hideLabel={{t('actions.hide')}}
89 | >
90 |
96 | {link}
97 |
98 |
99 |
100 |
101 |
102 |
103 | {t('node')} ({nodes.edges.length})
104 |
105 |
106 |
107 | {nodes.edges.map(({ id, name }) => (
108 |
116 | {name}
117 |
118 | ))}
119 |
120 |
121 |
122 |
123 |
124 | ))}
125 |
126 |
127 |
128 | {
133 | await importSubscriptionsMutation.mutateAsync(values.resources.map(({ link, tag }) => ({ link, tag })))
134 | }}
135 | />
136 |
137 | )
138 | }
139 |
--------------------------------------------------------------------------------
/src/pages/Orchestrate/index.tsx:
--------------------------------------------------------------------------------
1 | import { DndContext, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core'
2 | import { Badge, SimpleGrid, Stack, useMantineTheme } from '@mantine/core'
3 | import { useMediaQuery } from '@mantine/hooks'
4 | import { useMemo, useRef, useState } from 'react'
5 |
6 | import {
7 | useGroupAddNodesMutation,
8 | useGroupAddSubscriptionsMutation,
9 | useGroupsQuery,
10 | useNodesQuery,
11 | useSubscriptionsQuery,
12 | } from '~/apis'
13 | import { DraggableResourceType, DraggingResource } from '~/constants'
14 | import { restrictToElement } from '~/utils'
15 |
16 | import { Config } from './Config'
17 | import { DNS } from './DNS'
18 | import { GroupResource } from './Group'
19 | import { NodeResource } from './Node'
20 | import { Routing } from './Routing'
21 | import { SubscriptionResource } from './Subscription'
22 |
23 | export const OrchestratePage = () => {
24 | const { data: nodesQuery } = useNodesQuery()
25 | const { data: groupsQuery } = useGroupsQuery()
26 | const { data: subscriptionsQuery } = useSubscriptionsQuery()
27 |
28 | const groupAddNodesMutation = useGroupAddNodesMutation()
29 | const groupAddSubscriptionsMutation = useGroupAddSubscriptionsMutation()
30 |
31 | const [draggingResource, setDraggingResource] = useState(null)
32 |
33 | const draggingResourceDisplayName = useMemo(() => {
34 | if (draggingResource) {
35 | const { type, nodeID, groupID, subscriptionID } = draggingResource
36 |
37 | if (type === DraggableResourceType.node) {
38 | const node = nodesQuery?.nodes.edges.find((node) => node.id === nodeID)
39 |
40 | return node?.tag
41 | }
42 |
43 | if (type === DraggableResourceType.subscription) {
44 | const subscription = subscriptionsQuery?.subscriptions.find(
45 | (subscription) => subscription.id === subscriptionID,
46 | )
47 |
48 | return subscription?.tag || subscription?.link
49 | }
50 |
51 | if (type === DraggableResourceType.subscription_node) {
52 | const subscription = subscriptionsQuery?.subscriptions.find(
53 | (subscription) => subscription.id === subscriptionID,
54 | )
55 | const node = subscription?.nodes.edges.find((node) => node.id === nodeID)
56 |
57 | return node?.name
58 | }
59 |
60 | if (type === DraggableResourceType.groupNode) {
61 | const group = groupsQuery?.groups.find((group) => group.id === groupID)
62 |
63 | const node = group?.nodes.find((node) => node.id === nodeID)
64 |
65 | return node?.name
66 | }
67 |
68 | if (type === DraggableResourceType.groupSubscription) {
69 | const group = groupsQuery?.groups.find((group) => group.id === groupID)
70 |
71 | const subscription = group?.subscriptions.find((subscription) => subscription.id === subscriptionID)
72 |
73 | return subscription?.tag
74 | }
75 | }
76 | }, [draggingResource, groupsQuery?.groups, nodesQuery?.nodes.edges, subscriptionsQuery?.subscriptions])
77 |
78 | const onDragStart = (e: DragStartEvent) => {
79 | setDraggingResource({
80 | ...(e.active.data.current as DraggingResource),
81 | })
82 | }
83 |
84 | const onDragEnd = (e: DragEndEvent) => {
85 | const { over } = e
86 |
87 | if (over?.id && draggingResource) {
88 | const group = groupsQuery?.groups.find((group) => group.id === over.id)
89 |
90 | if (
91 | [DraggableResourceType.node, DraggableResourceType.groupNode].includes(draggingResource.type) &&
92 | draggingResource?.nodeID &&
93 | !group?.nodes.find((node) => node.id === draggingResource.nodeID)
94 | ) {
95 | groupAddNodesMutation.mutate({ id: over.id as string, nodeIDs: [draggingResource.nodeID] })
96 | }
97 |
98 | if (
99 | [DraggableResourceType.subscription, DraggableResourceType.groupSubscription].includes(draggingResource.type) &&
100 | draggingResource.subscriptionID &&
101 | !group?.subscriptions.find((subscription) => subscription.id === draggingResource.subscriptionID)
102 | ) {
103 | groupAddSubscriptionsMutation.mutate({
104 | id: over.id as string,
105 | subscriptionIDs: [draggingResource.subscriptionID],
106 | })
107 | }
108 |
109 | if (
110 | draggingResource.type === DraggableResourceType.subscription_node &&
111 | draggingResource.nodeID &&
112 | !group?.nodes.find((node) => node.id === draggingResource.nodeID)
113 | ) {
114 | groupAddNodesMutation.mutate({ id: over.id as string, nodeIDs: [draggingResource.nodeID] })
115 | }
116 | }
117 |
118 | setDraggingResource(null)
119 | }
120 |
121 | const dndAreaRef = useRef(null)
122 | const theme = useMantineTheme()
123 | const matchSmallScreen = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`)
124 |
125 | return (
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | {draggingResource && {draggingResourceDisplayName}}
141 |
142 |
143 |
144 |
145 | )
146 | }
147 |
--------------------------------------------------------------------------------
/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Experiment'
2 | export * from './MainLayout'
3 | export * from './Orchestrate'
4 | export * from './Setup'
5 |
--------------------------------------------------------------------------------
/src/schemas/gql/fragment-masking.ts:
--------------------------------------------------------------------------------
1 | import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'
2 | import { FragmentDefinitionNode } from 'graphql'
3 | import { Incremental } from './graphql'
4 |
5 | export type FragmentType> =
6 | TDocumentType extends DocumentTypeDecoration
7 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }]
8 | ? TKey extends string
9 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } }
10 | : never
11 | : never
12 | : never
13 |
14 | // return non-nullable if `fragmentType` is non-nullable
15 | export function useFragment(
16 | _documentNode: DocumentTypeDecoration,
17 | fragmentType: FragmentType>,
18 | ): TType
19 | // return nullable if `fragmentType` is nullable
20 | export function useFragment(
21 | _documentNode: DocumentTypeDecoration,
22 | fragmentType: FragmentType> | null | undefined,
23 | ): TType | null | undefined
24 | // return array of non-nullable if `fragmentType` is array of non-nullable
25 | export function useFragment(
26 | _documentNode: DocumentTypeDecoration,
27 | fragmentType: ReadonlyArray>>,
28 | ): ReadonlyArray
29 | // return array of nullable if `fragmentType` is array of nullable
30 | export function useFragment(
31 | _documentNode: DocumentTypeDecoration,
32 | fragmentType: ReadonlyArray>> | null | undefined,
33 | ): ReadonlyArray | null | undefined
34 | export function useFragment(
35 | _documentNode: DocumentTypeDecoration,
36 | fragmentType:
37 | | FragmentType>
38 | | ReadonlyArray>>
39 | | null
40 | | undefined,
41 | ): TType | ReadonlyArray | null | undefined {
42 | return fragmentType as any
43 | }
44 |
45 | export function makeFragmentData, FT extends ResultOf>(
46 | data: FT,
47 | _fragment: F,
48 | ): FragmentType {
49 | return data as FragmentType
50 | }
51 | export function isFragmentReady(
52 | queryNode: DocumentTypeDecoration,
53 | fragmentNode: TypedDocumentNode,
54 | data: FragmentType, any>> | null | undefined,
55 | ): data is FragmentType {
56 | const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__
57 | ?.deferredFields
58 |
59 | if (!deferredFields) return true
60 |
61 | const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined
62 | const fragName = fragDef?.name?.value
63 |
64 | const fields = (fragName && deferredFields[fragName]) || []
65 | return fields.length > 0 && fields.every((field) => data && field in data)
66 | }
67 |
--------------------------------------------------------------------------------
/src/schemas/gql/index.ts:
--------------------------------------------------------------------------------
1 | export * from './fragment-masking'
2 | export * from './gql'
3 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { UniqueIdentifier } from '@dnd-kit/core'
2 | import { ColorScheme } from '@mantine/core'
3 | import { persistentAtom, persistentMap } from '@nanostores/persistent'
4 | import { atom, map } from 'nanostores'
5 |
6 | import { COLS_PER_ROW, DEFAULT_ENDPOINT_URL, MODE } from '~/constants'
7 |
8 | export type PersistentSortableKeys = {
9 | nodeSortableKeys: UniqueIdentifier[]
10 | subscriptionSortableKeys: UniqueIdentifier[]
11 | configSortableKeys: UniqueIdentifier[]
12 | routingSortableKeys: UniqueIdentifier[]
13 | dnsSortableKeys: UniqueIdentifier[]
14 | groupSortableKeys: UniqueIdentifier[]
15 | }
16 |
17 | export type AppState = {
18 | preferredColorScheme: ColorScheme | ''
19 | colsPerRow: number
20 | } & PersistentSortableKeys
21 |
22 | export const modeAtom = persistentAtom('mode')
23 | export const tokenAtom = persistentAtom('token')
24 | export const endpointURLAtom = persistentAtom('endpointURL', DEFAULT_ENDPOINT_URL)
25 | export const appStateAtom = persistentMap(
26 | 'APP_STATE',
27 | {
28 | preferredColorScheme: '',
29 | colsPerRow: COLS_PER_ROW,
30 | nodeSortableKeys: [],
31 | subscriptionSortableKeys: [],
32 | configSortableKeys: [],
33 | routingSortableKeys: [],
34 | dnsSortableKeys: [],
35 | groupSortableKeys: [],
36 | },
37 | {
38 | encode: JSON.stringify,
39 | decode: JSON.parse,
40 | },
41 | )
42 |
43 | export type DEFAULT_RESOURCES = {
44 | defaultConfigID: string
45 | defaultRoutingID: string
46 | defaultDNSID: string
47 | defaultGroupID: string
48 | }
49 |
50 | export const defaultResourcesAtom = map({
51 | defaultConfigID: '',
52 | defaultRoutingID: '',
53 | defaultDNSID: '',
54 | defaultGroupID: '',
55 | })
56 |
57 | export const colorSchemeAtom = atom('dark')
58 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { UniqueIdentifier } from '@dnd-kit/core'
2 |
3 | import { defaultNS, resources } from '~/i18n'
4 |
5 | declare type SortableList = Array<{ id: UniqueIdentifier } & Record>
6 |
7 | declare type SimpleDisplayable = number | string
8 |
9 | declare type Displayable =
10 | | null
11 | | boolean
12 | | SimpleDisplayable
13 | | Array
14 | | Array<{ [key: string]: SimpleDisplayable }>
15 |
16 | declare module 'i18next' {
17 | interface CustomTypeOptions {
18 | defaultNS: typeof defaultNS
19 | resources: {
20 | translation: (typeof resources)['zh-Hans'][typeof defaultNS]
21 | }
22 | }
23 | }
24 |
25 | export type Optional = Omit & Partial>
26 |
--------------------------------------------------------------------------------
/src/utils/dnd-kit.ts:
--------------------------------------------------------------------------------
1 | import type { ClientRect, Modifier } from '@dnd-kit/core'
2 | import type { Transform } from '@dnd-kit/utilities'
3 |
4 | /**
5 | * This file is a copy taken from dnd-kits restrictToBoundingRect.ts
6 | */
7 | export const restrictToBoundingRect = (transform: Transform, rect: ClientRect, boundingRect: ClientRect): Transform => {
8 | const value = {
9 | ...transform,
10 | }
11 |
12 | if (rect.top + transform.y <= boundingRect.top) {
13 | value.y = boundingRect.top - rect.top
14 | } else if (rect.bottom + transform.y >= boundingRect.top + boundingRect.height) {
15 | value.y = boundingRect.top + boundingRect.height - rect.bottom
16 | }
17 |
18 | if (rect.left + transform.x <= boundingRect.left) {
19 | value.x = boundingRect.left - rect.left
20 | } else if (rect.right + transform.x >= boundingRect.left + boundingRect.width) {
21 | value.x = boundingRect.left + boundingRect.width - rect.right
22 | }
23 |
24 | return value
25 | }
26 |
27 | export const restrictToElement = (element: HTMLElement | null): Modifier => {
28 | return ({ draggingNodeRect, transform }) => {
29 | const parentRect = element ? (element.getBoundingClientRect() as ClientRect) : null
30 |
31 | if (!draggingNodeRect || !parentRect) {
32 | return transform
33 | }
34 |
35 | return restrictToBoundingRect(transform, draggingNodeRect, parentRect)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/utils/helper.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 |
3 | export class Defer {
4 | promise: Promise
5 | resolve?: (value: T | PromiseLike) => void
6 | reject?: (reason?: unknown) => void
7 |
8 | constructor() {
9 | this.promise = new Promise((resolve, reject) => {
10 | this.resolve = resolve
11 | this.reject = reject
12 | })
13 | }
14 | }
15 |
16 | export const fileToBase64 = (file: File) => {
17 | const reader = new FileReader()
18 | reader.readAsDataURL(file)
19 |
20 | const defer = new Defer()
21 |
22 | reader.onload = () => {
23 | if (defer.resolve) {
24 | defer.resolve(reader.result as string)
25 | }
26 | }
27 |
28 | reader.onerror = (err) => {
29 | if (defer.reject) {
30 | defer.reject(err)
31 | }
32 | }
33 |
34 | return defer.promise
35 | }
36 |
37 | const r = /([0-9]+)([a-z]+)/
38 |
39 | type Time = {
40 | hours: number
41 | minutes: number
42 | seconds: number
43 | milliseconds: number
44 | }
45 |
46 | export const parseDigitAndUnit = (
47 | timeStr: string,
48 | output: Time = { hours: 0, milliseconds: 0, minutes: 0, seconds: 0 },
49 | ): Time => {
50 | const matchRes = timeStr.match(r)
51 |
52 | if (!matchRes) {
53 | return output
54 | }
55 |
56 | const digit = Number.parseInt(matchRes[1])
57 | const unit = matchRes[2]
58 |
59 | switch (unit) {
60 | case 'h':
61 | output.hours = digit
62 | break
63 | case 'm':
64 | output.minutes = digit
65 | break
66 | case 's':
67 | output.seconds = digit
68 | break
69 | case 'ms':
70 | output.milliseconds = digit
71 | break
72 | }
73 |
74 | return parseDigitAndUnit(timeStr.replace(r, ''), output)
75 | }
76 |
77 | export const deriveTime = (timeStr: string, outputUnit: 'ms' | 's') =>
78 | dayjs.duration(parseDigitAndUnit(timeStr)).as(outputUnit === 'ms' ? 'milliseconds' : 'seconds')
79 |
--------------------------------------------------------------------------------
/src/utils/index.test.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import duration from 'dayjs/plugin/duration'
3 |
4 | import { deriveTime } from './helper'
5 |
6 | beforeAll(() => {
7 | dayjs.extend(duration)
8 | })
9 |
10 | test('deriveTime can parse hours', () => {
11 | expect(deriveTime('1h', 's')).toBe(3600)
12 | })
13 |
14 | test('deriveTime can parse minutes', () => {
15 | expect(deriveTime('1m', 's')).toBe(60)
16 | })
17 |
18 | test('deriveTime can parse seconds', () => {
19 | expect(deriveTime('60s', 's')).toBe(60)
20 | })
21 |
22 | test('deriveTime can parse multiple units combined', () => {
23 | expect(deriveTime('1h1m1s100ms', 's')).toBe(3661.1)
24 | })
25 |
26 | test('deriveTime can parse seconds to milliseconds', () => {
27 | expect(deriveTime('1s', 'ms')).toBe(1000)
28 | })
29 |
30 | test('deriveTime can parse milliseconds to seconds', () => {
31 | expect(deriveTime('1000ms', 's')).toBe(1)
32 | })
33 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dnd-kit'
2 | export * from './helper'
3 | export * from './node'
4 |
--------------------------------------------------------------------------------
/src/utils/node.ts:
--------------------------------------------------------------------------------
1 | import URI from 'urijs'
2 |
3 | export type GenerateURLParams = {
4 | username?: string
5 | password?: string
6 | protocol: string
7 | host: string
8 | port: number
9 | params?: Record
10 | hash: string
11 | path?: string
12 | }
13 |
14 | export const generateURL = ({ username, password, protocol, host, port, params, hash, path }: GenerateURLParams) => {
15 | /**
16 | * 所有参数设置默认值
17 | * 避免方法检测到参数为null/undefine返回该值查询结果
18 | * 查询结果当然不是URI类型,导致链式调用失败
19 | */
20 | const uri = URI()
21 | .protocol(protocol || 'http')
22 | .username(username || '')
23 | .password(password || '')
24 | .host(host || '')
25 | .port(String(port) || '80')
26 | .path(path || '')
27 | .query(params || {})
28 | .hash(hash || '')
29 |
30 | return uri.toString()
31 | }
32 |
33 | export const generateHysteria2URL = ({
34 | protocol,
35 | auth,
36 | host,
37 | port,
38 | params,
39 | }: {
40 | protocol: string
41 | auth: string
42 | host: string
43 | port: number
44 | params: Record
45 | }) => {
46 | // Encode the auth field to handle special characters like '@'
47 | const encodedAuth = encodeURIComponent(auth)
48 | const uri = new URL(`${protocol}://${encodedAuth}@${host}:${port}/`)
49 |
50 | Object.entries(params).forEach(([key, value]) => {
51 | if (value !== null && value !== undefined && value !== '') {
52 | uri.searchParams.append(key, String(value))
53 | }
54 | })
55 |
56 | return uri.toString()
57 | }
58 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowImportingTsExtensions": true,
4 | "baseUrl": "./",
5 | "isolatedModules": true,
6 | "jsx": "react-jsx",
7 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
8 | "module": "ESNext",
9 | "moduleResolution": "bundler",
10 | "noEmit": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "paths": { "~/*": ["src/*"] },
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ESNext",
19 | "types": ["vitest/globals"]
20 | },
21 | "include": ["src"],
22 | "references": [{ "path": "./tsconfig.node.json" }]
23 | }
24 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | import react from '@vitejs/plugin-react-swc'
4 | import { defineConfig } from 'vite'
5 |
6 | // eslint-disable-next-line import/no-default-export
7 | export default defineConfig(() => {
8 | return {
9 | base: './',
10 | resolve: { alias: { '~': path.resolve('src') } },
11 | plugins: [react()],
12 | test: { globals: true },
13 | }
14 | })
15 |
--------------------------------------------------------------------------------