├── .github ├── dependabot.yml └── workflows │ ├── docker-builder.yml │ ├── publish-docker.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE.txt ├── README.md ├── config-overrides.js ├── docker-entrypoint.sh ├── docker └── dockerfile.builder ├── generate_config_js.sh ├── licensing └── LICENSE.txt ├── netmaker.png ├── nginx.conf ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192192.png ├── logo256256.png ├── logo512512.png ├── manifest.json └── robots.txt ├── release.md ├── src ├── App.css ├── App.test.js ├── App.tsx ├── NM.png ├── components │ ├── Checkbox.ts │ ├── ConfirmDialog.tsx │ ├── CopyText.tsx │ ├── CustomSelect.tsx │ ├── Link.tsx │ ├── Loading.tsx │ ├── LoadingText.tsx │ ├── NetworkSelect.tsx │ ├── PathBreadcrumbs.tsx │ ├── Table.tsx │ ├── dashboard │ │ ├── ACLCard.tsx │ │ ├── AccessKeyCard.tsx │ │ ├── AdminCard.tsx │ │ ├── DNSCard.tsx │ │ ├── EnrollmentKeyCard.tsx │ │ ├── ExtClientsCard.tsx │ │ ├── GraphCard.tsx │ │ ├── NetworkCard.tsx │ │ ├── NodeCard.tsx │ │ └── UserCard.tsx │ ├── dialog │ │ └── CustomDialog.tsx │ ├── drawer │ │ └── CustomDrawer.tsx │ ├── filter │ │ └── Tablefilter.tsx │ ├── form │ │ ├── Form.tsx │ │ ├── FormCheckboxInput.tsx │ │ ├── FormList.tsx │ │ ├── FormOptionSelect.tsx │ │ ├── FormSwitchInput.tsx │ │ ├── FormTextInput.tsx │ │ ├── index.ts │ │ ├── internal │ │ │ └── formContext.ts │ │ └── validate.ts │ ├── index.ts │ ├── loading-text.css │ ├── theme.ts │ └── utils.tsx ├── config.ts ├── ee │ ├── LICENSE │ ├── metrics │ │ ├── MetricRoute.tsx │ │ ├── components │ │ │ └── MetricButton.tsx │ │ ├── util.tsx │ │ └── views │ │ │ ├── ExtMetrics.tsx │ │ │ ├── MetricsTable.tsx │ │ │ ├── NodeMetrics.tsx │ │ │ └── charts │ │ │ ├── ReceivedChart.tsx │ │ │ ├── SentChart.tsx │ │ │ └── Uptime.tsx │ ├── nodes │ │ └── FailoverButton.tsx │ └── users │ │ ├── UsersEE.tsx │ │ └── components │ │ ├── UserEditEE.tsx │ │ ├── UserGroupEdit.tsx │ │ └── UserTableEE.tsx ├── i18n │ ├── i18n.ts │ ├── resources │ │ └── en.ts │ └── type.ts ├── index.css ├── index.tsx ├── logo.svg ├── netmaker-logo-2.png ├── netmaker-logo.png ├── proroute │ ├── proaccesskeys │ │ ├── ProDashboardAccessKeys.tsx │ │ └── components │ │ │ ├── ProAccessKeyCreate.tsx │ │ │ ├── ProAccessKeyTable.tsx │ │ │ └── ProAccessKeyView.tsx │ ├── proaccessleveldashboards │ │ ├── NetUserSwitch.tsx │ │ └── components │ │ │ ├── AdminNetwork.tsx │ │ │ ├── ExtAccessCard.tsx │ │ │ ├── NodeAccessCard.tsx │ │ │ ├── ProAccessKeyCard.tsx │ │ │ └── WelcomeCard.tsx │ ├── prodrawer │ │ └── ProDrawerNotAdmin.tsx │ ├── prologin │ │ ├── CreateAdmin.tsx │ │ └── Login.tsx │ ├── pronetadmin │ │ ├── NetAdminDashboard.tsx │ │ ├── components │ │ │ ├── ProChoicesSelect.tsx │ │ │ ├── ProNetworkCreate.tsx │ │ │ ├── ProNetworkEdit.tsx │ │ │ ├── ProNetworkListEdit.tsx │ │ │ └── ProNetworkModifiedStats.tsx │ │ └── views │ │ │ └── ProNetworkTable.tsx │ ├── pronodeuser │ │ ├── NodeUserDashboard.tsx │ │ ├── components │ │ │ ├── NodeAccessView.tsx │ │ │ ├── ProNodeEdit.tsx │ │ │ ├── ProNodeId.tsx │ │ │ └── ProNodesView.tsx │ │ └── views │ │ │ ├── ProNodeEdit.tsx │ │ │ └── RemoteAccessView.tsx │ ├── prouser │ │ ├── ProUserView.tsx │ │ ├── VpnDashboard.tsx │ │ └── components │ │ │ ├── DeleteExtClientButtonVpn.tsx │ │ │ ├── DownloadExtClientButtonVpn.tsx │ │ │ ├── EditExtClientButtonVpn.tsx │ │ │ ├── ExtClientCreateButtonVpn.tsx │ │ │ ├── ExtClientEditVpn.tsx │ │ │ ├── ExtClientViewVpn.tsx │ │ │ ├── NetClientsViewVpn.tsx │ │ │ ├── NetworkSelectVpn.tsx │ │ │ ├── QrCodeViewVpn.tsx │ │ │ └── testdata.tsx │ └── proutils │ │ ├── ProConsts.tsx │ │ ├── ProCustomSelect.tsx │ │ └── ProNetworkSelect.tsx ├── react-app-env.d.ts ├── route │ ├── Header.tsx │ ├── PrivateRoute.tsx │ ├── accesskeys │ │ ├── AccessKeys.tsx │ │ └── components │ │ │ ├── AccessKeyCreate.tsx │ │ │ ├── AccessKeyDetails.tsx │ │ │ ├── AccessKeySelect.tsx │ │ │ ├── AccessKeyTable.tsx │ │ │ └── AccessKeyView.tsx │ ├── dashboard │ │ ├── Dashboard.tsx │ │ └── dashboard-styles.css │ ├── dns │ │ ├── DNS.tsx │ │ └── components │ │ │ ├── DNSCreate.tsx │ │ │ ├── DNSSelect.tsx │ │ │ └── DNSView.tsx │ ├── enrollmentkeys │ │ ├── EnrollmentKeysPage.tsx │ │ └── components │ │ │ ├── CreateEnrollmentKeyModal.tsx │ │ │ ├── EnrollmentKeyDetailsModal.tsx │ │ │ └── EnrollmentKeysTable.tsx │ ├── extclients │ │ ├── ExtClients.tsx │ │ └── components │ │ │ ├── DeleteExtClientButton.tsx │ │ │ ├── DownloadExtClientButton.tsx │ │ │ ├── EditExtClientButton.tsx │ │ │ ├── ExtClientCreateButton.tsx │ │ │ ├── ExtClientEdit.tsx │ │ │ ├── ExtClientView.tsx │ │ │ ├── NetClientsView.tsx │ │ │ ├── NetworkSelect.tsx │ │ │ └── QrCodeView.tsx │ ├── graph │ │ ├── Graphs.tsx │ │ └── components │ │ │ ├── NetworkGraph.tsx │ │ │ └── graph-components │ │ │ ├── NodeDetails.tsx │ │ │ ├── NodeGraph.tsx │ │ │ ├── types.tsx │ │ │ └── util.tsx │ ├── hosts │ │ ├── HostDetailPage.tsx │ │ ├── HostEditPage.tsx │ │ ├── HostsPage.tsx │ │ └── components │ │ │ ├── CreateRelayModal.tsx │ │ │ ├── HostNetworksTable.tsx │ │ │ ├── HostRelayTable.tsx │ │ │ └── HostsTable.tsx │ ├── login │ │ └── Login.tsx │ ├── logs │ │ └── ServerLogs.tsx │ ├── networks │ │ ├── Networks.tsx │ │ ├── components │ │ │ └── NetworkTable.tsx │ │ ├── create │ │ │ └── NetworkCreate.tsx │ │ └── networkId │ │ │ ├── NetworkId.tsx │ │ │ ├── accesskeys │ │ │ └── AccessKeys.tsx │ │ │ ├── components │ │ │ └── NetworkModifiedStats.tsx │ │ │ └── edit │ │ │ ├── ChoicesSelect.tsx │ │ │ ├── NetworkEdit.tsx │ │ │ └── NetworkListEdit.tsx │ ├── networkusers │ │ ├── NetworkUsers.tsx │ │ └── components │ │ │ ├── ChoicesSelect.tsx │ │ │ ├── NetworkUserEdit.tsx │ │ │ └── NetworkUsersTable.tsx │ ├── node_acls │ │ ├── NodeACLs.tsx │ │ └── components │ │ │ └── NodesACLTable.tsx │ ├── nodes │ │ ├── Nodes.tsx │ │ ├── components │ │ │ └── NodeTable.tsx │ │ └── netid │ │ │ ├── NetworkNodes.tsx │ │ │ ├── components │ │ │ ├── CreateEgress.tsx │ │ │ ├── CreateRelay.tsx │ │ │ ├── HubButton.tsx │ │ │ ├── RelaySelect.tsx │ │ │ └── TableToggleButton.tsx │ │ │ ├── nodeEdit │ │ │ └── NodeEdit.tsx │ │ │ └── nodeId │ │ │ └── NodeId.tsx │ ├── root.tsx │ ├── usergroups │ │ ├── UserGroups.tsx │ │ ├── create │ │ │ └── UserGroupCreate.tsx │ │ └── table │ │ │ └── UserGroupsTable.tsx │ └── users │ │ ├── UsersCommunity.tsx │ │ ├── components │ │ └── UserTableCommunity.tsx │ │ ├── create │ │ └── UserCreate.tsx │ │ └── userId │ │ ├── UserChangePassword.tsx │ │ └── UserEditCommunity.tsx ├── serviceWorker.js ├── setupTests.js ├── store │ ├── actions.ts │ ├── createStore.ts │ ├── modules │ │ ├── acls │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── auth │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── enrollmentkeys │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── hosts │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── network │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── node │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── pro │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── router │ │ │ ├── Component.tsx │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── server │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── saga.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ └── toast │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── saga.ts │ │ │ └── types.ts │ ├── reducers.ts │ ├── sagas.ts │ ├── selectors.ts │ └── types.ts ├── types │ └── react-app-env.d.ts └── util │ ├── enrollmentkeys.ts │ ├── errorpage.tsx │ ├── fields.ts │ ├── genericerror.tsx │ ├── hosts.ts │ ├── network.ts │ ├── node.ts │ ├── query.ts │ ├── regex.ts │ └── unixTime.ts ├── tsconfig.json └── tsconfig.paths.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | target-branch: "develop" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "develop" 13 | - package-ecosystem: "docker" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | target-branch: "develop" 18 | -------------------------------------------------------------------------------- /.github/workflows/docker-builder.yml: -------------------------------------------------------------------------------- 1 | name: Build go-builder images 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '00 21 * * SUN' 7 | 8 | jobs: 9 | go-builder: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: SetUp Buildx 15 | uses: docker/setup-buildx-action@v1 16 | - name: Login to Dockerhub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | - name: Build and push to docker hub 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | push: true 26 | platforms: linux/amd64, linux/arm64 27 | file: ./docker/dockerfile.builder 28 | tags: gravitl/go-ui-builder:latest -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: 'docker tag' 8 | required: true 9 | release: 10 | types: [published] 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Set tag 18 | run: | 19 | if [[ -n "${{ github.event.inputs.tag }}" ]]; then 20 | TAG=${{ github.event.inputs.tag }} 21 | elif [[ "${{ github.ref_name }}" == 'master' ]]; then 22 | TAG="latest" 23 | else 24 | TAG="${{ github.ref_name }}" 25 | fi 26 | echo "TAG=${TAG}" >> $GITHUB_ENV 27 | - 28 | name: Checkout 29 | uses: actions/checkout@v3 30 | - 31 | name: Set up QEMU 32 | uses: docker/setup-qemu-action@v2 33 | - 34 | name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v2 36 | - 37 | name: Login to DockerHub 38 | uses: docker/login-action@v2 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USERNAME }} 41 | password: ${{ secrets.DOCKERHUB_TOKEN }} 42 | - 43 | name: Build and push 44 | uses: docker/build-push-action@v4 45 | with: 46 | context: . 47 | platforms: linux/amd64, linux/arm64, linux/arm/v7 48 | push: true 49 | tags: ${{ github.repository }}:${{ env.TAG }}, ${{ github.repository }}:latest 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | release: 2 | runs-on: ubuntu-latest 3 | permissions: 4 | contents: write 5 | steps: 6 | - name: checkout 7 | uses: actions/checkout@v3 8 | with: 9 | ref: develop 10 | - name: setup git 11 | run: | 12 | git config user.name "Github Actions" 13 | git config user.email "info@netmaker.io" 14 | - name: create branch 15 | git switch -c release_${{ inputs.version }} 16 | git tag -f ${{ inputs.version }} 17 | #push branch 18 | git push origin release_${{ inputs.version }} 19 | #push tag 20 | git push origin ${{ inputs.version }} 21 | 22 | - name: release assets 23 | uses: ncipollo/release-action@v1 24 | with: 25 | bodyFile: "release.md" 26 | name: ${{ inputs.version }} 27 | tag: ${{ inputs.version }} 28 | commit: develop 29 | - name: pull request 30 | env: 31 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: | 33 | gh api --method POST \ 34 | -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' \ 35 | /repos/${{ github.repository }}/pulls \ 36 | -f title='${{ inputs.version }}' \ 37 | -f head='release_${{ inputs.version }}' \ 38 | -f base="master" 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .vscode/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17.1.0 as builder 2 | 3 | LABEL \ 4 | org.opencontainers.image.authors="Dillon Carns & Alex Feiszli, Gravitl, inc." \ 5 | org.opencontainers.image.vendor="ReactJS" \ 6 | org.opencontainers.image.url="local" \ 7 | org.opencontainers.image.source="https://dockerhub.com/" \ 8 | org.opencontainers.image.version="$VERSION" \ 9 | org.opencontainers.image.revision="$REVISION" \ 10 | vendor="ReactJS" \ 11 | name="Netmaker" \ 12 | version="$VERSION-$REVISION" \ 13 | summary="The frontend of Netmaker. Netmaker builds fast, secure virtual networks." \ 14 | description="This image contains the Netmaker frontend running with the ReactJS runtime. 2021 - Gravitl, inc." 15 | 16 | RUN mkdir /usr/src/app 17 | WORKDIR /usr/src/app 18 | ENV PATH /usr/src/app/node_modules/.bin:$PATH 19 | COPY package*.json ./ 20 | RUN npm install --silent 21 | RUN npm install react-scripts@4.0.3 -g --silent 22 | COPY . /usr/src/app 23 | ENV NODE_OPTIONS "--openssl-legacy-provider" 24 | RUN npm run build 25 | 26 | FROM nginx:1.23-alpine 27 | # RUN rm -rf /etc/nginx/conf.d 28 | COPY nginx.conf /etc/nginx/conf.d/default.conf 29 | COPY --from=builder /usr/src/app/build /usr/share/nginx/html 30 | COPY docker-entrypoint.sh generate_config_js.sh / 31 | RUN chmod +x docker-entrypoint.sh generate_config_js.sh 32 | ENTRYPOINT ["/docker-entrypoint.sh"] 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## v0.19.0 6 | 7 | ## For more information, visit one of the official documentation sites: 8 | 9 | 1. [docs.netmaker.org](https://docs.netmaker.org) 10 | 2. [netmaker.readthedocs.io](https://netmaker.readthedocs.io) 11 | 12 | ## To run dev server: 13 | 14 | 1. `npm i` 15 | 2. `npm start` or `NODE_OPTIONS=--openssl-legacy-provider npm start` for newer versions of node due [this bug](https://github.com/webpack/webpack/issues/14532) in webpack 16 | 17 | Runs the app in the development mode. 18 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 19 | 20 | The page will reload if you make edits. 21 | You will also see any lint errors in the console. 22 | 23 | ## To build static files: 24 | 25 | 1. `npm i` 26 | 2. `npm run build` 27 | 28 | #### LICENSE 29 | 30 | Netmaker UI's source code and all artifacts in this repository are freely available. All versions are published under the Server Side Public License (SSPL), version 1, which can be found under the "licensing" directory: [LICENSE.txt](licensing/LICENSE.txt). 31 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const {alias, configPaths} = require('react-app-rewire-alias') 2 | 3 | module.exports = function override(config) { 4 | return alias(configPaths('./tsconfig.paths.json'))(config) 5 | }; 6 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | ./generate_config_js.sh >/usr/share/nginx/html/config.js 3 | echo ">>>> backend set to: $BACKEND_URL <<<<<" 4 | nginx -g "daemon off;" 5 | -------------------------------------------------------------------------------- /docker/dockerfile.builder: -------------------------------------------------------------------------------- 1 | FROM node:17.1.0 2 | 3 | LABEL \ 4 | org.opencontainers.image.authors="Dillon Carns & Alex Feiszli, Gravitl, inc." \ 5 | org.opencontainers.image.vendor="ReactJS" \ 6 | org.opencontainers.image.url="local" \ 7 | org.opencontainers.image.source="https://dockerhub.com/" \ 8 | org.opencontainers.image.version="$VERSION" \ 9 | org.opencontainers.image.revision="$REVISION" \ 10 | vendor="ReactJS" \ 11 | name="Netmaker" \ 12 | version="$VERSION-$REVISION" \ 13 | summary="The frontend of Netmaker. Netmaker builds fast, secure virtual networks." \ 14 | description="This image contains the Netmaker frontend running with the ReactJS runtime. 2021 - Gravitl, inc." 15 | 16 | RUN mkdir /usr/src/app 17 | WORKDIR /usr/src/app 18 | ENV PATH /usr/src/app/node_modules/.bin:$PATH 19 | COPY package*.json ./ 20 | RUN npm install --silent 21 | RUN npm install react-scripts@4.0.3 -g --silent -------------------------------------------------------------------------------- /generate_config_js.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | if [ -z "${BACKEND_URL:-}" ]; then 3 | BACKEND_URL="http://localhost:8081" 4 | fi 5 | 6 | cat <0.2%", 62 | "not dead", 63 | "not op_mini all" 64 | ], 65 | "development": [ 66 | "last 1 chrome version", 67 | "last 1 firefox version", 68 | "last 1 safari version" 69 | ] 70 | }, 71 | "devDependencies": { 72 | "@testing-library/jest-dom": "^4.2.4", 73 | "@testing-library/react": "^9.5.0", 74 | "@testing-library/user-event": "^7.2.1", 75 | "@types/jest": "^27.0.2", 76 | "@types/node": "^18.16.0", 77 | "@types/react": "^17.0.24", 78 | "@types/react-dom": "^17.0.9", 79 | "@types/react-router-dom": "^5.3.0", 80 | "@types/redux-logger": "^3.0.9", 81 | "@welldone-software/why-did-you-render": "^6.2.1", 82 | "prettier": "^2.4.1", 83 | "react-app-rewire-alias": "^1.1.4", 84 | "react-app-rewired": "^2.1.8" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravitl/netmaker-ui/9083e5e0f8202df166e4b664dded6b401d4fc74f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | 19 | Netmaker 20 | 21 | 22 | 23 |
24 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/logo192192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravitl/netmaker-ui/9083e5e0f8202df166e4b664dded6b401d4fc74f/public/logo192192.png -------------------------------------------------------------------------------- /public/logo256256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravitl/netmaker-ui/9083e5e0f8202df166e4b664dded6b401d4fc74f/public/logo256256.png -------------------------------------------------------------------------------- /public/logo512512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravitl/netmaker-ui/9083e5e0f8202df166e4b664dded6b401d4fc74f/public/logo512512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Netmaker UI", 3 | "name": "Netmaker Management UI", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | # Netmaker-UI v0.19.0 2 | 3 | ## whats new 4 | dependencies updated 5 | 6 | ## whats fixed 7 | 8 | ## known issues 9 | 10 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body::-webkit-scrollbar { 2 | width: 1em; 3 | } 4 | 5 | body::-webkit-scrollbar-track { 6 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 7 | } 8 | 9 | body::-webkit-scrollbar-thumb { 10 | background-color: darkgrey; 11 | outline: 1px solid slategrey; 12 | } 13 | 14 | .selected { 15 | background-color: transparent; 16 | } 17 | 18 | .selected:hover { 19 | background: #139da4; 20 | } 21 | 22 | .MuiInputBase-fromControl { 23 | height: 50vh; 24 | } 25 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import App from './App' 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render() 7 | const linkElement = getByText(/learn react/i) 8 | expect(linkElement).toBeInTheDocument() 9 | }) 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './App.css' 4 | import 'react-toastify/dist/ReactToastify.css' 5 | 6 | import { ToastContainer } from 'react-toastify' 7 | import { ThemeProvider } from '@mui/material/styles' 8 | import { useCurrentTheme } from './components/theme' 9 | import { useSelector, useDispatch } from 'react-redux' 10 | import { authSelectors, nodeSelectors, aclSelectors } from '~store/types' 11 | import { logout } from '~store/modules/auth/actions' 12 | import AdapterDateFns from '@mui/lab/AdapterDateFns' 13 | import LocalizationProvider from '@mui/lab/LocalizationProvider' 14 | 15 | import Routes from './route/root' 16 | import { PathBreadcrumbsProvider } from './components/PathBreadcrumbs' 17 | import { getNodes } from '~store/modules/node/actions' 18 | import { getHosts } from '~store/modules/hosts/actions' 19 | 20 | function App() { 21 | const dispatch = useDispatch() 22 | const user = useSelector(authSelectors.getUser) 23 | const token = useSelector(authSelectors.getToken) 24 | const isLoggedIn = useSelector(authSelectors.getLoggedIn) 25 | const shouldSignOut = useSelector(nodeSelectors.getShouldSignOut) 26 | const currentACL = useSelector(aclSelectors.getCurrentACL) 27 | const userSettings = useSelector(authSelectors.getUserSettings) 28 | 29 | React.useEffect(() => { 30 | const interval = setInterval(() => { 31 | 32 | if (!isLoggedIn || (!!user && Date.now() / 1000 > user.exp && !window.location.href.includes('/login'))) { 33 | dispatch(logout()) 34 | } else if (shouldSignOut) { 35 | dispatch(logout()) 36 | } else if (isLoggedIn && token && user && user.isAdmin) { 37 | dispatch(getNodes.request({ token })) 38 | dispatch(getHosts.request()) 39 | } 40 | 41 | }, 7500) 42 | return () => clearInterval(interval) 43 | }, [dispatch, user, isLoggedIn, token, shouldSignOut, currentACL]) 44 | 45 | const theme = useCurrentTheme(userSettings.mode || 'dark') 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export default App 62 | -------------------------------------------------------------------------------- /src/NM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravitl/netmaker-ui/9083e5e0f8202df166e4b664dded6b401d4fc74f/src/NM.png -------------------------------------------------------------------------------- /src/components/Checkbox.ts: -------------------------------------------------------------------------------- 1 | import Checkbox from '@mui/material/Checkbox' 2 | import { styled } from '@mui/material/styles' 3 | 4 | export const NmCheckbox = styled(Checkbox)(({ theme }) => ({ 5 | color: theme.status.danger, 6 | '&.Mui-checked': { 7 | color: theme.status.danger, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /src/components/ConfirmDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react' 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | Button, 8 | Box, 9 | IconButton, 10 | Typography, 11 | } from '@mui/material' 12 | import { Close } from '@mui/icons-material' 13 | import { useTranslation } from 'react-i18next' 14 | import { TFunctionResult } from 'i18next' 15 | 16 | interface Props { 17 | title?: string 18 | message: string | React.ReactChild | TFunctionResult 19 | onCancel?: () => void 20 | onSubmit: () => void 21 | } 22 | 23 | const ConfirmDialog: React.FC = ({ 24 | visible, 25 | title, 26 | message, 27 | onCancel, 28 | onSubmit, 29 | }) => { 30 | const { t } = useTranslation() 31 | return ( 32 | 33 | {title ? title : t('dialog.title')} 34 | 35 | 36 | 37 | 38 | 39 | 40 | {typeof message === 'string' ? ( 41 | {message} 42 | ) : ( 43 | message 44 | )} 45 | 46 | 47 | 50 | 53 | 54 | 55 | ) 56 | } 57 | 58 | export const useDialog = () => { 59 | const [props, setProps] = useState(undefined) 60 | 61 | const onCancel = useCallback(() => { 62 | if (props) { 63 | props.onCancel?.() 64 | setProps(undefined) 65 | } 66 | }, [props, setProps]) 67 | 68 | const onSubmit = useCallback(() => { 69 | if (props) { 70 | props.onSubmit() 71 | setProps(undefined) 72 | } 73 | }, [props, setProps]) 74 | 75 | if (props) { 76 | return { 77 | Component: () => ( 78 | 84 | ), 85 | setProps, 86 | } 87 | } 88 | return { 89 | Component: () => ( 90 | 96 | ), 97 | setProps, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/components/CopyText.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Tooltip } from '@mui/material' 2 | import Typography from '@mui/material/Typography' 3 | import { TypographyVariant } from '@mui/material' 4 | import copy from 'copy-to-clipboard' 5 | import { Button } from '@mui/material' 6 | import { useTranslation } from 'react-i18next' 7 | import { Fragment } from 'react' 8 | 9 | export interface CopyProps { 10 | value: string 11 | color?: string 12 | type: TypographyVariant 13 | } 14 | 15 | export default function CopyText(Props: CopyProps) { 16 | const { t } = useTranslation() 17 | 18 | return ( 19 | 20 |
copy(Props.value)}> 21 | 30 |
31 |
32 | ) 33 | } 34 | 35 | export interface MulitCopyProps { 36 | color?: string 37 | type: TypographyVariant 38 | values: Array 39 | fullWidth?: boolean 40 | } 41 | 42 | export function MultiCopy({ color, type, values, fullWidth }: MulitCopyProps) { 43 | const isFullWidth = fullWidth === undefined ? true : fullWidth 44 | 45 | return ( 46 | 47 | {values.map((value, i) => 48 | !!value ? ( 49 | 50 | {isFullWidth && ( 51 | 52 | 53 | 54 | )} 55 | {!isFullWidth && ( 56 | 57 | )} 58 | 59 | ) : null 60 | )} 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/components/CustomSelect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Theme, useTheme } from '@mui/material/styles' 3 | import OutlinedInput from '@mui/material/OutlinedInput' 4 | import MenuItem from '@mui/material/MenuItem' 5 | import FormControl from '@mui/material/FormControl' 6 | import Select, { SelectChangeEvent } from '@mui/material/Select' 7 | import { useTranslation } from 'react-i18next' 8 | 9 | const ITEM_HEIGHT = 48 10 | const ITEM_PADDING_TOP = 8 11 | const MenuProps = { 12 | PaperProps: { 13 | style: { 14 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, 15 | width: 250, 16 | }, 17 | }, 18 | } 19 | 20 | function getStyles(name: string, personName: readonly string[], theme: Theme) { 21 | return { 22 | fontWeight: 23 | personName.indexOf(name) === -1 24 | ? theme.typography.fontWeightRegular 25 | : theme.typography.fontWeightMedium, 26 | } 27 | } 28 | 29 | export default function CustomSelect(Props: { 30 | items: string[] 31 | onSelect: (s: string) => void 32 | placeholder: string 33 | }) { 34 | const theme = useTheme() 35 | const [selectedItem, setSelectedItem] = React.useState([]) 36 | const { t } = useTranslation() 37 | const handleChange = (event: SelectChangeEvent) => { 38 | const { 39 | target: { value }, 40 | } = event 41 | setSelectedItem( 42 | // On autofill we get a the stringified value. 43 | typeof value === 'string' ? value.split(',') : value 44 | ) 45 | Props.onSelect(value as string) 46 | } 47 | 48 | return ( 49 |
50 | 51 | 79 | 80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { LinkProps, useHistory, useLocation } from 'react-router-dom' 3 | import { Button, ButtonProps } from '@mui/material' 4 | import { Theme } from '@mui/material/styles' 5 | import { SxProps } from '@mui/system' 6 | 7 | export const NmLink: React.FC< 8 | Exclude & ButtonProps & { sx?: SxProps } 9 | > = ({ sx, to, children, ...rest }) => { 10 | const history = useHistory() 11 | const location = useLocation() 12 | const navigate = useCallback(() => { 13 | history.push(typeof to === 'function' ? to(location) : to) 14 | }, [to, location, history]) 15 | 16 | return ( 17 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@mui/material' 2 | import Typography from '@mui/material/Typography' 3 | import { LinearProgress } from '@mui/material' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | export default function Loading() { 7 | const { t } = useTranslation() 8 | 9 | return ( 10 | 11 | 12 |
19 | {`${t('common.loading')}`} 20 |
21 |
22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/LoadingText.tsx: -------------------------------------------------------------------------------- 1 | import './loading-text.css' 2 | 3 | interface LoadingTextProps { 4 | text: string 5 | } 6 | 7 | export default function LoadingText(props: LoadingTextProps) { 8 | if (props.text) { 9 | return <>{props.text} 10 | } 11 | return ( 12 |
13 |
14 |
15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/filter/Tablefilter.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowDownward, ArrowUpward } from '@mui/icons-material' 2 | import { 3 | FormControl, 4 | InputLabel, 5 | Select, 6 | MenuItem, 7 | Grid, 8 | IconButton, 9 | Tooltip, 10 | } from '@mui/material' 11 | import { SelectChangeEvent } from '@mui/material/Select' 12 | import { useTranslation } from 'react-i18next' 13 | 14 | export interface TablefilterProps { 15 | values: string[] 16 | onSelect: (selection: string) => void 17 | onAscendClick: () => void 18 | ascending: boolean 19 | currentValue: string 20 | } 21 | 22 | export function Tablefilter(props: TablefilterProps) { 23 | const { values, ascending, onSelect, onAscendClick, currentValue } = props 24 | const { t } = useTranslation() 25 | 26 | return ( 27 | 28 | 29 | 30 | {t('common.sortby')} 31 | 41 | 42 | 43 | 44 | 51 | 52 | {ascending ? ( 53 | 54 | ) : ( 55 | 56 | )} 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/components/form/FormCheckboxInput.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import CheckboxField from '@mui/material/Checkbox' 3 | import { Controller } from 'react-hook-form' 4 | import FormControlLabel from '@mui/material/FormControlLabel' 5 | import { useFormControl } from './internal/formContext' 6 | 7 | export const NmFormInputCheckbox: FC<{ 8 | name: string 9 | label: string 10 | disabled?: boolean 11 | }> = ({ name, label, disabled }) => { 12 | const { control, disabled: formDisabled } = useFormControl() 13 | return ( 14 | ( 18 | 27 | } 28 | label={label} 29 | /> 30 | )} 31 | /> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/form/FormList.tsx: -------------------------------------------------------------------------------- 1 | import { useFieldArray } from 'react-hook-form' 2 | import { useFormControl } from './internal/formContext' 3 | import { FormControlLabelProps } from '@mui/material' 4 | import { Modify } from 'src/types/react-app-env' 5 | 6 | export function NmFormList({ 7 | name, 8 | children, 9 | disabled, 10 | }: { 11 | name: string 12 | disabled?: boolean 13 | labelPlacement?: FormControlLabelProps['labelPlacement'] 14 | children: ( 15 | fields: Array< 16 | Modify string }> 17 | >, 18 | disabled: boolean 19 | ) => JSX.Element 20 | }) { 21 | const { control, disabled: formDisabled } = useFormControl() 22 | const { fields } = useFieldArray({ 23 | control, 24 | name, 25 | }) 26 | 27 | return children( 28 | fields.map( 29 | (field, index) => 30 | ({ 31 | ...field, 32 | getFieldName: (path: string) => `${name}.${index}.${path}`, 33 | } as any) 34 | ), 35 | formDisabled || !!disabled 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/form/FormOptionSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TextFieldProps } from '@mui/material/TextField/TextField' 3 | import { Controller } from 'react-hook-form' 4 | import { useFormControl } from './internal/formContext' 5 | import { FormControl, InputLabel, MenuItem, Select } from '@mui/material' 6 | 7 | export const NmFormOptionSelect: React.FC< 8 | Omit< 9 | TextFieldProps, 10 | 'name' | 'onChange' | 'value' | 'error' | 'helperText' | 'rightAlign' 11 | > & { 12 | name: string 13 | rightAlign?: boolean 14 | selections: { key: string; option: any }[] 15 | } 16 | > = ({ name, disabled, rightAlign, selections, ...textfieldProps }) => { 17 | const { control, disabled: formDisabled } = useFormControl() 18 | 19 | return ( 20 | ( 24 | 25 | {textfieldProps.label} 26 | 41 | 42 | )} 43 | /> 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/components/form/FormSwitchInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SwitchField from '@mui/material/Switch' 3 | import { Controller } from 'react-hook-form' 4 | import FormControlLabel from '@mui/material/FormControlLabel' 5 | import { useFormControl } from './internal/formContext' 6 | import { FormControlLabelProps } from '@mui/material' 7 | 8 | export const NmFormInputSwitch: React.FC<{ 9 | name: string 10 | label: string 11 | disabled?: boolean 12 | labelPlacement?: FormControlLabelProps['labelPlacement'] 13 | defaultValue?: boolean 14 | }> = ({ name, label, disabled, labelPlacement, defaultValue }) => { 15 | const { control, disabled: formDisabled } = useFormControl() 16 | return ( 17 | ( 21 | 31 | } 32 | label={label} 33 | /> 34 | )} 35 | /> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/form/FormTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TextField, { TextFieldProps } from '@mui/material/TextField/TextField' 3 | import { Controller } from 'react-hook-form' 4 | import { FormContext } from './internal/formContext' 5 | 6 | export const NmFormInputText: React.FC< 7 | Omit< 8 | TextFieldProps, 9 | 'name' | 'onChange' | 'value' | 'error' | 'helperText' | 'rightAlign' 10 | > & { 11 | name: string 12 | rightAlign?: boolean 13 | } 14 | > = ({ name, disabled, rightAlign, ...textfieldProps }) => { 15 | 16 | return ( 17 | 18 | {({ control, disabled: formDisabled }) => ( 19 | ( 23 | 41 | )} 42 | /> 43 | )} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Form' 2 | export * from './FormCheckboxInput' 3 | export * from './FormSwitchInput' 4 | export * from './FormTextInput' 5 | export * from './validate' 6 | export * from './FormList' 7 | -------------------------------------------------------------------------------- /src/components/form/internal/formContext.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { Control } from 'react-hook-form' 3 | 4 | export const FormContext = React.createContext<{ 5 | control: Control 6 | disabled: boolean 7 | }>({ disabled: false, control: {} as any }) 8 | 9 | export const useFormControl = () => useContext(FormContext) 10 | -------------------------------------------------------------------------------- /src/components/form/validate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UnpackNestedValue, 3 | FieldError, 4 | ResolverResult, 5 | FieldErrors, 6 | DeepMap, 7 | UnionLike, 8 | DeepPartial, 9 | Resolver, 10 | } from 'react-hook-form' 11 | 12 | export function validate(validators: { 13 | [K in keyof UnpackNestedValue]?: ( 14 | value: UnpackNestedValue[K], 15 | formData: UnpackNestedValue 16 | ) => 17 | | DeepMap>, FieldError>[keyof DeepMap< 18 | DeepPartial>, 19 | FieldError 20 | >] 21 | | undefined 22 | }): Resolver { 23 | return (data: UnpackNestedValue): ResolverResult => { 24 | const keys = Object.keys(data) as Array> 25 | let hasError = false 26 | const errors = {} as FieldErrors //{ [K in keyof T]?: ({ message: string, type: string } | undefined)}; 27 | for (let i = 0; i < keys.length; i++) { 28 | const key = keys[i] 29 | const validator = validators[key] 30 | if (validator) { 31 | const error = validator(data[key], data) 32 | if (error) { 33 | hasError = true 34 | errors[key as keyof FieldErrors] = error 35 | } 36 | } 37 | } 38 | 39 | if (hasError) { 40 | return { 41 | errors, 42 | values: {}, 43 | } 44 | } 45 | return { 46 | errors: {}, 47 | values: data, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { NmLink } from './Link' 2 | export { NmCheckbox } from './Checkbox' 3 | export { darkTheme, lightTheme, useCurrentTheme } from './theme' 4 | -------------------------------------------------------------------------------- /src/components/loading-text.css: -------------------------------------------------------------------------------- 1 | @keyframes pulsate-fwd { 2 | 0% { 3 | width: 2px; 4 | height: 2px; 5 | } 6 | 50% { 7 | width: 4px; 8 | height: 4px; 9 | } 10 | 100% { 11 | width: 2px; 12 | height: 2px; 13 | } 14 | } 15 | 16 | .loading-text { 17 | display: flex; 18 | align-items: center; 19 | height: 2rem; 20 | } 21 | 22 | .loading-text .dot-1, 23 | .loading-text .dot-2, 24 | .loading-text .dot-3 { 25 | width: 2px; 26 | height: 2px; 27 | background-color: teal; 28 | border-radius: 50%; 29 | margin-right: 4px; 30 | animation: pulsate-fwd 0.7s ease-in-out infinite both; 31 | } 32 | 33 | .loading-text .dot-2 { 34 | animation-delay: 100ms; 35 | } 36 | 37 | .loading-text .dot-3 { 38 | animation-delay: 200ms; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/theme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material/styles' 2 | import { orange } from '@mui/material/colors' 3 | import { ThemeOptions, PaletteMode } from '@mui/material' 4 | 5 | // file for theme choices 6 | const netmakerColors = { 7 | purple: '#73509f', 8 | blue: '#119da4', 9 | green: '#0ead69', 10 | orange: '#fd4c41', 11 | black: '#110b35', 12 | white: '#f5f5f5', 13 | } 14 | 15 | export const darkTheme = createTheme({ 16 | status: { 17 | danger: orange[500], 18 | }, 19 | palette: { 20 | mode: 'dark' as PaletteMode, 21 | primary: { 22 | main: netmakerColors.blue, 23 | }, 24 | secondary: { 25 | main: netmakerColors.orange, 26 | }, 27 | common: { 28 | black: netmakerColors.black, 29 | white: netmakerColors.white, 30 | }, 31 | // background: { 32 | // paper: netmakerColors.black, 33 | // }, 34 | }, 35 | } as ThemeOptions) 36 | 37 | export const lightTheme = createTheme({ 38 | palette: { 39 | mode: 'light' as PaletteMode, 40 | primary: { 41 | main: netmakerColors.blue, 42 | }, 43 | secondary: { 44 | main: netmakerColors.orange, 45 | }, 46 | common: { 47 | black: netmakerColors.black, 48 | white: netmakerColors.white, 49 | }, 50 | }, 51 | status: { 52 | danger: netmakerColors.orange, 53 | }, 54 | } as ThemeOptions) 55 | 56 | // let currentTheme = darkTheme 57 | 58 | export const useCurrentTheme = (mode: 'light' | 'dark') => { 59 | if (mode === 'light') { 60 | return lightTheme 61 | } 62 | return darkTheme 63 | } 64 | 65 | // export const useSetTheme = () => (theme: Theme) => (currentTheme = theme) 66 | -------------------------------------------------------------------------------- /src/components/utils.tsx: -------------------------------------------------------------------------------- 1 | 2 | export const MAX_ATTEMPTS = 10 3 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json' 2 | // window.REACT_APP_BACKEND for container usage 3 | // if running locally or statically, change the 'http://localhost:8081' to desired backend URL and then build 4 | export const BACKEND_URL = 5 | (window as any).REACT_APP_BACKEND || 'http://localhost:8081' 6 | 7 | export const DEBUG = BACKEND_URL.includes('localhost') 8 | 9 | const extractVersion = (v: string | undefined) => { 10 | if (!!!v) { 11 | return 'latest' 12 | } 13 | if (v.charAt(0) === 'v' || v.charAt(0) === 'V') { 14 | return v 15 | } 16 | return `v${v}` 17 | } 18 | 19 | // == set UI version here == 20 | export const UI_VERSION = extractVersion(version) 21 | 22 | // == local storage keys == 23 | export const USER_KEY = 'netmaker-user' 24 | export const SETTINGS_KEY = 'netmaker-settings' 25 | -------------------------------------------------------------------------------- /src/ee/LICENSE: -------------------------------------------------------------------------------- 1 | The Netmaker Enterprise license (the “Enterprise License”) 2 | Copyright (c) 2022 Netmaker, Inc. 3 | 4 | With regard to the Netmaker Software: 5 | 6 | This software and associated documentation files (the "Software") may only be used in production, if you (and any entity that you represent) have agreed to, and are in compliance with, the Netmaker Subscription Terms of Service, available at https://netmaker.io/terms (the “Enterprise Terms”), or other agreement governing the use of the Software, as agreed by you and Netmaker, and otherwise have a valid Netmaker Enterprise license for the correct number of users, networks, nodes, servers, and external clients. Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software. You agree that Netmaker and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Netmaker Enterprise license for the correct number of users, networks, nodes, servers, and external clients as allocated by the license. Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes, without requiring a subscription. You agree that Netmaker and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications. You are not granted any other rights beyond what is expressly stated herein. Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | For all third party components incorporated into the Netmaker Software, those components are licensed under the original license provided by the owner of the applicable component. 11 | -------------------------------------------------------------------------------- /src/ee/metrics/MetricRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { NetworkSelect } from '~components/NetworkSelect' 7 | import { MetricsTable } from './views/MetricsTable' 8 | import { NodeMetrics } from './views/NodeMetrics' 9 | import { ExtMetrics } from './views/ExtMetrics' 10 | 11 | export const MetricRoute: React.FC = () => { 12 | const { path } = useRouteMatch() 13 | const { t } = useTranslation() 14 | 15 | useLinkBreadcrumb({ 16 | title: t('pro.metrics'), 17 | }) 18 | 19 | const titleStyle = { 20 | textAlign: 'center', 21 | } as any 22 | 23 | return ( 24 | 25 | 26 | 27 | 33 | 34 |
35 | {t('pro.metrics')} 36 |
37 |
38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 |
69 |
70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/ee/metrics/components/MetricButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@mui/material' 2 | import React from 'react' 3 | import { useHistory } from 'react-router-dom' 4 | import { useDispatch } from 'react-redux' 5 | import { clearCurrentMetrics } from '~store/modules/server/actions' 6 | 7 | type Props = { 8 | link: string 9 | text: string | undefined 10 | sx?: any 11 | logic?: () => void 12 | } 13 | 14 | export default function MetricButton(props: Props) { 15 | const dispatch = useDispatch() 16 | const history = useHistory() 17 | 18 | return () 31 | } 32 | -------------------------------------------------------------------------------- /src/ee/metrics/util.tsx: -------------------------------------------------------------------------------- 1 | // Utils for metrics 2 | export const getTimeMinHrs = (duration: number) => { 3 | const minutes = duration / 60000000000 4 | const hours = Math.floor(minutes / 60) 5 | const remainingMinutes = Math.ceil(((minutes / 60) % 1) * 60) 6 | return {hours, min: remainingMinutes} 7 | } 8 | -------------------------------------------------------------------------------- /src/ee/metrics/views/charts/ReceivedChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | BarElement, 7 | Title, 8 | Tooltip, 9 | Legend, 10 | } from 'chart.js'; 11 | import { Bar } from 'react-chartjs-2'; 12 | import { getTimeMinHrs } from '../../util'; 13 | 14 | ChartJS.register( 15 | CategoryScale, 16 | LinearScale, 17 | BarElement, 18 | Title, 19 | Tooltip, 20 | Legend 21 | ); 22 | 23 | export const options = { 24 | responsive: true, 25 | plugins: { 26 | legend: { 27 | position: 'top' as const, 28 | }, 29 | title: { 30 | display: true, 31 | text: 'Total Transmitted Data on Node', 32 | }, 33 | }, 34 | }; 35 | 36 | const labels = ['Received']; 37 | 38 | export const data = { 39 | labels, 40 | datasets: [ 41 | { 42 | label: 'Received', 43 | data: [100], 44 | backgroundColor: 'rgba(255, 99, 132, 0.5)', 45 | }, 46 | { 47 | label: 'Received per Hour', 48 | data: [Math.floor(Math.random() * 1000)], 49 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 50 | }, 51 | ], 52 | }; 53 | 54 | export const getData = (totalReceived: number, duration: number) => { 55 | const { hours, min } = getTimeMinHrs(duration) 56 | const totalTimeHours = hours + (min/60) 57 | const receivedPerHour = !!!duration ? 0 : totalReceived / totalTimeHours 58 | return ({ 59 | labels, 60 | datasets: [ 61 | { 62 | label: 'Received', 63 | data: [totalReceived], 64 | backgroundColor: 'rgba(255, 99, 132, 0.5)', 65 | }, 66 | { 67 | label: 'Received per Hour', 68 | data: [receivedPerHour], 69 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 70 | }, 71 | ], 72 | })} 73 | 74 | type ReceivedProps = { 75 | totalReceived: number 76 | duration: number 77 | } 78 | 79 | export function ReceivedChart({ totalReceived, duration }: ReceivedProps) { 80 | return ; 81 | } 82 | -------------------------------------------------------------------------------- /src/ee/metrics/views/charts/SentChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | BarElement, 7 | Title, 8 | Tooltip, 9 | Legend, 10 | } from 'chart.js'; 11 | import { Bar } from 'react-chartjs-2'; 12 | import { getTimeMinHrs } from '../../util' 13 | 14 | ChartJS.register( 15 | CategoryScale, 16 | LinearScale, 17 | BarElement, 18 | Title, 19 | Tooltip, 20 | Legend 21 | ); 22 | 23 | export const options = { 24 | responsive: true, 25 | plugins: { 26 | legend: { 27 | position: 'top' as const, 28 | }, 29 | title: { 30 | display: true, 31 | text: 'Total Transmitted Data on Node', 32 | }, 33 | }, 34 | }; 35 | 36 | const labels = ['Sent']; 37 | 38 | export const getData = (totalSent: number, duration: number) => { 39 | const { hours, min } = getTimeMinHrs(duration) 40 | const totalTimeHours = hours + (min/60) 41 | const sentPerHour = !!!duration ? 0 : totalSent / totalTimeHours 42 | return ({ 43 | labels, 44 | datasets: [ 45 | { 46 | label: 'Sent', 47 | data: [totalSent], 48 | backgroundColor: 'rgba(255, 99, 132, 0.5)', 49 | }, 50 | { 51 | label: 'Sent per Hour', 52 | data: [sentPerHour], 53 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 54 | }, 55 | ], 56 | })} 57 | 58 | type SentProps = { 59 | totalSent: number 60 | duration: number 61 | } 62 | 63 | export function SentChart({totalSent, duration}: SentProps) { 64 | return ; 65 | } 66 | -------------------------------------------------------------------------------- /src/ee/metrics/views/charts/Uptime.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material"; 2 | import { Chart, registerables } from "chart.js"; 3 | import React from "react"; 4 | import { Doughnut } from 'react-chartjs-2' 5 | import { getTimeMinHrs } from "../../util"; 6 | 7 | Chart.register(...registerables); 8 | 9 | interface Props { 10 | chartData: number[]; 11 | actualUptime?: number 12 | } 13 | 14 | const styles = { 15 | doughContainer: { 16 | width: "40%", 17 | height: "40%", 18 | top: "50%", 19 | left: "50%", 20 | position: "absolute", 21 | transform: "translate(-50%, -25%)" 22 | }, 23 | relative: { 24 | position: "relative" 25 | } 26 | } as any 27 | 28 | const options = { 29 | plugins: { 30 | legend: { 31 | display: false 32 | }, 33 | }, 34 | cutout: 55, 35 | elements: { 36 | arc: { 37 | borderWidth: 0, 38 | } 39 | } 40 | }; 41 | 42 | export const UptimeChart = (props: Props) => { 43 | // helper function to format chart data since you do this twice 44 | const formatData = (data: number[]) => ({ 45 | responsive: true, 46 | labels: ["Up", "Down"], 47 | datasets: [{ 48 | label: 'Uptime', 49 | data: data, 50 | backgroundColor: [ 51 | 'rgb(54, 255, 120)', 52 | 'rgb(255, 99, 132)', 53 | ], 54 | hoverOffset: 4 55 | }] 56 | }); 57 | 58 | let timeString = "" 59 | if (!!props.actualUptime) { 60 | const {hours, min} = getTimeMinHrs(props.actualUptime) 61 | timeString = `${hours}h${min}m` 62 | } else { 63 | timeString= '0h0m' 64 | } 65 | 66 | return ( 67 |
68 |
69 | 70 | {!!timeString &&
71 | 72 | {timeString} 73 | 74 |
} 75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/ee/nodes/FailoverButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch } from 'react-redux' 3 | import { useTranslation } from 'react-i18next' 4 | import { IconButton, Tooltip } from '@mui/material' 5 | import { Block, Check, AirlineStops } from '@mui/icons-material' 6 | import { 7 | createIngressNode, deleteIngressNode, 8 | } from '~modules/node/actions' 9 | import { Node } from '~store/types' 10 | import CustomDialog from '~components/dialog/CustomDialog' 11 | 12 | const hoverRedStyle = { 13 | ':hover': { 14 | color: 'red', 15 | }, 16 | } 17 | 18 | const hoverBlueStyle = { 19 | ':hover': { 20 | color: '#3f51b5', 21 | }, 22 | } 23 | 24 | export const FailoverButton: React.FC<{ 25 | node: Node 26 | }> = ({ 27 | node, 28 | }) => { 29 | const dispatch = useDispatch() 30 | const [hovering, setHovering] = React.useState(false) 31 | const [open, setOpen] = React.useState(false) 32 | const { t } = useTranslation() 33 | 34 | const handleHoverEnter = () => setHovering(true) 35 | const handleHoverLeave = () => setHovering(false) 36 | 37 | const handleOpen = () => setOpen(true) 38 | const handleClose = () => setOpen(false) 39 | 40 | const createIngress = () => { 41 | dispatch( 42 | createIngressNode.request({ 43 | netid: node.network, 44 | nodeid: node.id, 45 | failover: true, 46 | }) 47 | ) 48 | } 49 | 50 | const removeAction = () => { 51 | dispatch( 52 | deleteIngressNode.request({ 53 | netid: node.network, 54 | nodeid: node.id, 55 | }) 56 | ) 57 | } 58 | 59 | return ( 60 | <> 61 | (!node.failover ? createIngress() : removeAction())} 65 | message={`${t('pro.helpers.failover')}`} 66 | title={`${!node.failover ? t('common.create') : t('common.delete')} ${t('node.failover')}`} 67 | /> 68 | 69 | 76 | {!node.failover ? : hovering ? : } 77 | 78 | 79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n, { Resource } from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import { produce } from 'immer' 4 | 5 | import { en } from './resources/en' 6 | 7 | // the translations 8 | // (tip move them in a JSON file and import them, 9 | // or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) 10 | const resources: Resource = { 11 | en: { 12 | translation: en, 13 | }, 14 | } 15 | 16 | // Used to check for missing keys in dev consolse 17 | ;(window as any).missingTranslations = {} 18 | 19 | i18n 20 | .use(initReactI18next) // passes i18n down to react-i18next 21 | .init({ 22 | resources, 23 | lng: 'en', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources 24 | // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage 25 | // if you're using a language detector, do not define the lng option 26 | saveMissing: true, 27 | missingKeyHandler: (lngs, ns, key, fallbackValue) => { 28 | ;(window as any).missingTranslations = produce<{}, any>( 29 | (window as any).missingTranslations, 30 | (draftState) => { 31 | draftState[`${key}`] = fallbackValue 32 | } 33 | ) 34 | }, 35 | interpolation: { 36 | escapeValue: false, // react already safes from xss 37 | }, 38 | appendNamespaceToMissingKey: true, 39 | }) 40 | 41 | export { i18n } 42 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import * as serviceWorker from './serviceWorker' 6 | import { createReduxStore } from './store/createStore' 7 | import { Provider } from 'react-redux' 8 | import { BrowserRouter } from 'react-router-dom' 9 | import './i18n/i18n' 10 | 11 | const store = createReduxStore() 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ) 23 | 24 | // If you want your app to work offline and load faster, you can change 25 | // unregister() to register() below. Note this comes with some pitfalls. 26 | // Learn more about service workers: https://bit.ly/CRA-PWA 27 | serviceWorker.unregister() 28 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/netmaker-logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravitl/netmaker-ui/9083e5e0f8202df166e4b664dded6b401d4fc74f/src/netmaker-logo-2.png -------------------------------------------------------------------------------- /src/netmaker-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravitl/netmaker-ui/9083e5e0f8202df166e4b664dded6b401d4fc74f/src/netmaker-logo.png -------------------------------------------------------------------------------- /src/proroute/proaccesskeys/ProDashboardAccessKeys.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { ProAccessKeyCreate } from './components/ProAccessKeyCreate' 7 | import { ProAccessKeyTable } from './components/ProAccessKeyTable' 8 | import { ProAccessKeyView } from './components/ProAccessKeyView' 9 | 10 | export const ProDashboardAccessKeys: React.FC = () => { 11 | const { path } = useRouteMatch() 12 | const { t } = useTranslation() 13 | 14 | useLinkBreadcrumb({ 15 | title: t('breadcrumbs.accessKeys'), 16 | }) 17 | 18 | return ( 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/proroute/proaccesskeys/components/ProAccessKeyView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { proSelectors } from '~store/types' 4 | import { useHistory, useParams } from 'react-router' 5 | import AccessKeyDetails from '../../../route/accesskeys/components/AccessKeyDetails' 6 | import { useTranslation } from 'react-i18next' 7 | import { NotFound } from '~util/errorpage' 8 | 9 | export const ProAccessKeyView: React.FC<{}> = () => { 10 | const history = useHistory() 11 | const { t } = useTranslation() 12 | const { netid, keyname } = useParams<{ netid: string; keyname: string }>() 13 | const userData = useSelector(proSelectors.networkUserData)[netid] 14 | if (!!!userData) { 15 | return 16 | } 17 | const networks = userData.networks 18 | const netIndex = networks.findIndex((network) => network.netid === netid) 19 | const accessKeys = networks[netIndex].accesskeys 20 | const keyIndex = accessKeys.findIndex( 21 | (accessKey) => accessKey.name === keyname 22 | ) 23 | const accessKeyInfo = accessKeys[keyIndex] 24 | 25 | const handleClose = () => history.goBack() 26 | 27 | return ( 28 | {}} 32 | handleClose={handleClose} 33 | accessString={accessKeyInfo.accessstring} 34 | keyValue={accessKeyInfo.value} 35 | /> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/proroute/proaccessleveldashboards/NetUserSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { useParams } from 'react-router-dom' 4 | import { proSelectors } from '~store/selectors' 5 | import { NetAdminDashboard } from '../pronetadmin/NetAdminDashboard' 6 | import { NodeUserDashboard } from '../pronodeuser/NodeUserDashboard' 7 | import { 8 | NET_ADMIN_ACCESS_LVL, 9 | NODE_ACCESS_LVL, 10 | CLIENT_ACCESS_LVL, 11 | } from '../proutils/ProConsts' 12 | 13 | import { NotFound } from '~util/errorpage' 14 | import { VpnDashboard } from '../prouser/VpnDashboard' 15 | 16 | export default function NetUserView() { 17 | const { netid } = useParams<{ netid: string }>() 18 | const allUserData = useSelector(proSelectors.networkUserData) 19 | 20 | if (!!!allUserData || !!!allUserData[netid]) { 21 | return 22 | } 23 | 24 | const userData = allUserData[netid] 25 | 26 | switch (userData.user.accesslevel) { 27 | case NET_ADMIN_ACCESS_LVL: 28 | return 29 | case NODE_ACCESS_LVL: 30 | return 31 | case CLIENT_ACCESS_LVL: 32 | return 33 | default: 34 | return
NO ACCESS
35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/proroute/proaccessleveldashboards/components/AdminNetwork.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link, useParams } from 'react-router-dom' 3 | import Avatar from '@mui/material/Avatar' 4 | import Card from '@mui/material/Card' 5 | 6 | import { grey } from '@mui/material/colors' 7 | 8 | import CardContent from '@mui/material/CardContent' 9 | import Typography from '@mui/material/Typography' 10 | import { useTranslation } from 'react-i18next' 11 | import { Wifi } from '@mui/icons-material' 12 | import { Button, useTheme } from '@mui/material' 13 | 14 | export default function AdminNetworkCard() { 15 | const { t } = useTranslation() 16 | const theme = useTheme() 17 | const { netid } = useParams<{ netid: string }>() 18 | 19 | const cardStyle = { 20 | marginBottom: '1em', 21 | marginTop: '1em', 22 | height: '100%', 23 | minHeight: '14em', 24 | width: '100%', 25 | padding: '.5em', 26 | } 27 | 28 | const cardContentStyle = { 29 | display: 'flex', 30 | justifyContent: 'center', 31 | alignItems: 'center', 32 | flexDirection: 'column', 33 | } as any 34 | 35 | return ( 36 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/proroute/proaccessleveldashboards/components/ExtAccessCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link, useParams } from 'react-router-dom' 3 | import Card from '@mui/material/Card' 4 | 5 | // import CreateIcon from '@mui/icons-material/AddBox'; 6 | import { grey } from '@mui/material/colors' 7 | import Avatar from '@mui/material/Avatar' 8 | import { ExternalClient } from '~store/types' 9 | 10 | import CardContent from '@mui/material/CardContent' 11 | import Typography from '@mui/material/Typography' 12 | import { useTranslation } from 'react-i18next' 13 | import { Devices } from '@mui/icons-material' 14 | import { Button, useTheme } from '@mui/material' 15 | 16 | export default function ExtAccessCard(Props: { clients: ExternalClient[] }) { 17 | const { t } = useTranslation() 18 | const theme = useTheme() 19 | const { netid } = useParams<{ netid: string }>() 20 | 21 | const cardStyle = { 22 | marginBottom: '1em', 23 | marginTop: '1em', 24 | height: '100%', 25 | width: '100%', 26 | minHeight: '14em', 27 | } 28 | 29 | const cardContentStyle = { 30 | display: 'flex', 31 | justifyContent: 'center', 32 | alignItems: 'center', 33 | flexDirection: 'column', 34 | } as any 35 | 36 | return ( 37 | 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/proroute/proaccessleveldashboards/components/NodeAccessCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link, useParams } from 'react-router-dom' 3 | import Card from '@mui/material/Card' 4 | import Avatar from '@mui/material/Avatar' 5 | 6 | import { grey } from '@mui/material/colors' 7 | import CardContent from '@mui/material/CardContent' 8 | import Typography from '@mui/material/Typography' 9 | import { useTranslation } from 'react-i18next' 10 | import { DeviceHub } from '@mui/icons-material' 11 | import { Button, useTheme } from '@mui/material' 12 | 13 | export default function NodeAccessCard() { 14 | const { t } = useTranslation() 15 | const theme = useTheme() 16 | const { netid } = useParams<{ netid: string }>() 17 | 18 | const cardStyle = { 19 | marginBottom: '1em', 20 | marginTop: '1em', 21 | height: '100%', 22 | width: '100%', 23 | minHeight: '14em', 24 | } 25 | 26 | const cardContentStyle = { 27 | display: 'flex', 28 | justifyContent: 'center', 29 | alignItems: 'center', 30 | flexDirection: 'column', 31 | } as any 32 | 33 | return ( 34 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/proroute/proaccessleveldashboards/components/ProAccessKeyCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link, useParams } from 'react-router-dom' 3 | import Card from '@mui/material/Card' 4 | 5 | // import CreateIcon from '@mui/icons-material/AddBox'; 6 | import { grey } from '@mui/material/colors' 7 | import Avatar from '@mui/material/Avatar' 8 | import CardContent from '@mui/material/CardContent' 9 | import Typography from '@mui/material/Typography' 10 | import { useTranslation } from 'react-i18next' 11 | import { VpnKey } from '@mui/icons-material' 12 | import { Button, useTheme } from '@mui/material' 13 | 14 | export default function ProAccessKeyCard() { 15 | const { t } = useTranslation() 16 | const theme = useTheme() 17 | const { netid } = useParams<{ netid: string }>() 18 | 19 | const cardStyle = { 20 | marginBottom: '1em', 21 | marginTop: '1em', 22 | width: '100%', 23 | height: '100%', 24 | minHeight: '14em', 25 | } 26 | 27 | const cardContentStyle = { 28 | display: 'flex', 29 | justifyContent: 'center', 30 | alignItems: 'center', 31 | flexDirection: 'column', 32 | } as any 33 | 34 | return ( 35 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/proroute/proaccessleveldashboards/components/WelcomeCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import Avatar from '@mui/material/Avatar' 4 | import Card from '@mui/material/Card' 5 | import { grey } from '@mui/material/colors' 6 | import { useSelector } from 'react-redux' 7 | import CardContent from '@mui/material/CardContent' 8 | import Typography from '@mui/material/Typography' 9 | import { useTranslation } from 'react-i18next' 10 | import ThreePIcon from '@mui/icons-material/ThreeP' 11 | import { Button, useTheme } from '@mui/material' 12 | import { authSelectors } from '~store/selectors' 13 | 14 | export default function WelcomeCard() { 15 | const { t } = useTranslation() 16 | const theme = useTheme() 17 | const user = useSelector(authSelectors.getUser) 18 | 19 | const cardStyle = { 20 | marginBottom: '1em', 21 | marginTop: '1em', 22 | height: '100%', 23 | width: '50%', 24 | MaxWidth: '50%', 25 | minHeight: '15em', 26 | } 27 | 28 | const cardContentStyle = { 29 | display: 'flex', 30 | justifyContent: 'center', 31 | alignItems: 'center', 32 | flexDirection: 'column', 33 | } as any 34 | 35 | return ( 36 | 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/proroute/pronetadmin/components/ProNetworkModifiedStats.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, TextField } from '@mui/material' 2 | import { useTranslation } from 'react-i18next' 3 | import { datePickerConverter } from '~util/unixTime' 4 | import { useNetwork } from '~util/network' 5 | 6 | export const ProNetworkModifiedStats: React.FC<{ netid: string }> = ({ 7 | netid, 8 | }) => { 9 | const network = useNetwork(netid) 10 | const { t } = useTranslation() 11 | 12 | const fieldStyle = { 13 | marginTop: '1em', 14 | } 15 | 16 | if (!network) { 17 | return null 18 | } 19 | return ( 20 | 27 | 28 | 38 | 39 | 40 | 41 | 51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/proroute/pronodeuser/NodeUserDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route, useParams } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import NodeAccessCard from '../proaccessleveldashboards/components/NodeAccessCard' 7 | import ExtAccessCard from '../proaccessleveldashboards/components/ExtAccessCard' 8 | import { useSelector } from 'react-redux' 9 | import { proSelectors } from '~store/selectors' 10 | import { ExternalClient } from '~store/types' 11 | import { RemoteAccessView } from './views/RemoteAccessView' 12 | import { ProNodesView } from './components/ProNodesView' 13 | 14 | export const NodeUserDashboard: React.FC = () => { 15 | const { path } = useRouteMatch() 16 | const { t } = useTranslation() 17 | const { netid } = useParams<{ netid: string }>() 18 | const netData = useSelector(proSelectors.networkUserData)[netid] 19 | let clients = [] as ExternalClient[] 20 | 21 | if (!!netData) { 22 | clients = netData.clients 23 | } 24 | 25 | useLinkBreadcrumb({ 26 | title: t('breadcrumbs.nodeuserdashboard'), 27 | }) 28 | 29 | return ( 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/proroute/pronodeuser/components/ProNodesView.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route, useParams } from 'react-router-dom' 4 | import { useSelector } from 'react-redux' 5 | import { proSelectors } from '~store/selectors' 6 | import { Node } from '~store/types' 7 | import { NodeAccessView } from './NodeAccessView' 8 | import { NET_ADMIN_ACCESS_LVL } from '../../proutils/ProConsts' 9 | import { ProNodeId } from './ProNodeId' 10 | import { NotFound } from '~util/errorpage' 11 | import { ProNodeEdit } from './ProNodeEdit' 12 | 13 | export const ProNodesView: React.FC = () => { 14 | const { path } = useRouteMatch() 15 | const { netid } = useParams<{ netid: string }>() 16 | const netData = useSelector(proSelectors.networkUserData)[netid] 17 | let nodes = [] as Node[] 18 | 19 | if (!!netData) { 20 | nodes = netData.nodes 21 | } else { 22 | return 23 | } 24 | 25 | return ( 26 | 27 | 28 | 29 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {netData.user.accesslevel === NET_ADMIN_ACCESS_LVL && ( 52 | 53 | 54 | 55 | 56 | 57 | )} 58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/proroute/pronodeuser/views/RemoteAccessView.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route, useParams } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { ExtClientViewVpn } from '../../../proroute/prouser/components/ExtClientViewVpn' 7 | import { useSelector } from 'react-redux' 8 | import { proSelectors } from '~store/selectors' 9 | import { ExternalClient, Node } from '~store/types' 10 | import { ExtClientEditVpn } from '../../../proroute/prouser/components/ExtClientEditVpn' 11 | import { QrCodeViewVpn } from '../../../proroute/prouser/components/QrCodeViewVpn' 12 | 13 | export const RemoteAccessView: React.FC = () => { 14 | const { path } = useRouteMatch() 15 | const { t } = useTranslation() 16 | const { netid } = useParams<{ netid: string }>() 17 | const netData = useSelector(proSelectors.networkUserData)[netid] 18 | let clients = [] as ExternalClient[] 19 | let vpns = [] as Node[] 20 | 21 | if (!!netData) { 22 | clients = netData.clients 23 | vpns = netData.vpns 24 | } 25 | 26 | useLinkBreadcrumb({ 27 | title: t('breadcrumbs.netadmindashboard'), 28 | }) 29 | 30 | return ( 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/proroute/prouser/ProUserView.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { ProNetworkSelect } from '../proutils/ProNetworkSelect' 7 | import { useSelector } from 'react-redux' 8 | import { proSelectors } from '~store/types' 9 | import Loading from '~components/Loading' 10 | import NetUserView from '../proaccessleveldashboards/NetUserSwitch' 11 | 12 | export const ProUserView: React.FC = () => { 13 | const { path } = useRouteMatch() 14 | const { t } = useTranslation() 15 | const isLoading = useSelector(proSelectors.isProcessing) 16 | 17 | useLinkBreadcrumb({ 18 | title: t('breadcrumbs.userdashboard'), 19 | }) 20 | 21 | if (isLoading) return 22 | 23 | return ( 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/proroute/prouser/VpnDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route, useParams } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import ExtAccessCard from '../proaccessleveldashboards/components/ExtAccessCard' 7 | import { useSelector } from 'react-redux' 8 | import { proSelectors } from '~store/selectors' 9 | import { ExternalClient } from '~store/types' 10 | import { RemoteAccessView } from '../pronodeuser/views/RemoteAccessView' 11 | 12 | export const VpnDashboard: React.FC = () => { 13 | const { path } = useRouteMatch() 14 | const { t } = useTranslation() 15 | const { netid } = useParams<{ netid: string }>() 16 | const netData = useSelector(proSelectors.networkUserData)[netid] 17 | let clients = [] as ExternalClient[] 18 | 19 | if (!!netData) { 20 | clients = netData.clients 21 | } 22 | 23 | useLinkBreadcrumb({ 24 | title: t('breadcrumbs.netadmindashboard'), 25 | }) 26 | 27 | return ( 28 | 29 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/proroute/prouser/components/DeleteExtClientButtonVpn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Delete } from '@mui/icons-material' 3 | import { IconButton, Tooltip } from '@mui/material' 4 | import { useDispatch } from 'react-redux' 5 | import { i18n } from '../../../i18n/i18n' 6 | import { deleteExternalClient } from '~store/modules/node/actions' 7 | import { ExternalClient } from '~store/types' 8 | import CustomizedDialogs from '~components/dialog/CustomDialog' 9 | 10 | export const DeleteExtClientButtonVpn: React.FC<{ 11 | client: ExternalClient 12 | }> = ({ client }) => { 13 | const dispatch = useDispatch() 14 | const [open, setOpen] = React.useState(false) 15 | 16 | const handleDeleteClient = () => { 17 | dispatch( 18 | deleteExternalClient.request({ 19 | clientName: client.clientid, 20 | netid: client.network, 21 | }) 22 | ) 23 | } 24 | 25 | const handleClose = () => setOpen(false) 26 | 27 | const handleOpen = () => setOpen(true) 28 | 29 | return ( 30 | <> 31 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/proroute/prouser/components/DownloadExtClientButtonVpn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Download, QrCode2 } from '@mui/icons-material' 3 | import { IconButton, Tooltip } from '@mui/material' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import { i18n } from '../../../i18n/i18n' 6 | import { getExternalClientConf } from '~store/modules/node/actions' 7 | import { authSelectors, ExternalClient } from '~store/types' 8 | import { useHistory, useRouteMatch } from 'react-router' 9 | 10 | export const DownloadExtClientButtonVpn: React.FC<{ 11 | client: ExternalClient 12 | type: 'qr' | 'file' 13 | }> = ({ client, type }) => { 14 | const dispatch = useDispatch() 15 | const token = useSelector(authSelectors.getToken) 16 | const history = useHistory() 17 | const { url } = useRouteMatch() 18 | 19 | const handleDownloadConf = () => { 20 | dispatch( 21 | getExternalClientConf.request({ 22 | clientid: client.clientid, 23 | netid: client.network, 24 | type, 25 | token: token || '', 26 | }) 27 | ) 28 | 29 | if (type === 'qr') { 30 | history.push(`${url}/${client.clientid}/qr`) 31 | } 32 | } 33 | 34 | return ( 35 | 43 | 44 | {type === 'file' ? : } 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/proroute/prouser/components/EditExtClientButtonVpn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tooltip } from '@mui/material' 3 | import { i18n } from '../../../i18n/i18n' 4 | import { ExternalClient } from '~store/types' 5 | import { useRouteMatch } from 'react-router' 6 | import { NmLink } from '~components/Link' 7 | 8 | export const EditExtClientButtonVpn: React.FC<{ 9 | client: ExternalClient 10 | }> = ({ client }) => { 11 | const { url } = useRouteMatch() 12 | 13 | return ( 14 | 18 | 23 | {client.clientid} 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/proroute/prouser/components/ExtClientCreateButtonVpn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AddCircleOutline } from '@mui/icons-material' 3 | import { IconButton, Tooltip } from '@mui/material' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import { useTranslation } from 'react-i18next' 6 | import { createExternalClient } from '~store/modules/node/actions' 7 | import { Node, proSelectors } from '~store/types' 8 | import { useParams } from 'react-router-dom' 9 | 10 | export const ExtClientCreateButtonVpn: React.FC<{ 11 | node: Node 12 | }> = ({ node }) => { 13 | const dispatch = useDispatch() 14 | const userData = useSelector(proSelectors.networkUserData) 15 | const { netid } = useParams<{ netid: string }>() 16 | const { t } = useTranslation() 17 | 18 | const data = userData[netid] 19 | 20 | const clientCount = data.clients.length 21 | const clientsLeft = data.user.clientlimit - clientCount 22 | const clientDisable = clientsLeft <= 0 23 | 24 | const handleExtClientCreate = () => { 25 | dispatch( 26 | createExternalClient.request({ 27 | netid: node.network, 28 | nodeid: node.id, 29 | }) 30 | ) 31 | } 32 | 33 | return ( 34 | 42 | 47 | 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/proroute/prouser/components/NetClientsViewVpn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { useRouteMatch, useHistory } from 'react-router-dom' 4 | import { networkSelectors } from '~store/types' 5 | import { useTranslation } from 'react-i18next' 6 | import CustomSelect from '../../../components/CustomSelect' 7 | import { Grid } from '@mui/material' 8 | 9 | export const NetClientsVpn: React.FC = () => { 10 | const listOfNetworks = useSelector(networkSelectors.getNetworks) 11 | const networkNames = [] 12 | if (listOfNetworks) { 13 | for (let i = 0; i < listOfNetworks.length; i++) { 14 | networkNames.push(listOfNetworks[i].netid) 15 | } 16 | } 17 | const { path } = useRouteMatch() 18 | const { t } = useTranslation() 19 | const history = useHistory() 20 | 21 | const centerStyle = { 22 | textAlign: 'center', 23 | } as any 24 | 25 | return ( 26 | 27 | 28 | { 31 | history.push(`${path}/${selected}`) 32 | }} 33 | items={networkNames} 34 | /> 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/proroute/prouser/components/NetworkSelectVpn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { useRouteMatch, useHistory } from 'react-router-dom' 4 | import { networkSelectors } from '~store/types' 5 | import { useTranslation } from 'react-i18next' 6 | import CustomSelect from '../../../components/CustomSelect' 7 | import { Grid } from '@mui/material' 8 | 9 | export const NetworkSelectVpn: React.FC = () => { 10 | const listOfNetworks = useSelector(networkSelectors.getNetworks) 11 | const networkNames = [] 12 | if (listOfNetworks) { 13 | for (let i = 0; i < listOfNetworks.length; i++) { 14 | networkNames.push(listOfNetworks[i].netid) 15 | } 16 | } 17 | const { path } = useRouteMatch() 18 | const { t } = useTranslation() 19 | const history = useHistory() 20 | 21 | const centerStyle = { 22 | textAlign: 'center', 23 | } as any 24 | 25 | return ( 26 | 27 | 28 | { 31 | history.push(`${path}/${selected}`) 32 | }} 33 | items={networkNames} 34 | /> 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/proroute/prouser/components/QrCodeViewVpn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Grid, Modal, Typography, Box } from '@mui/material' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { useTranslation } from 'react-i18next' 5 | import { useHistory, useParams } from 'react-router' 6 | import { nodeSelectors } from '~store/types' 7 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 8 | import { clearQr } from '~store/modules/node/actions' 9 | import { NotFound } from '~util/errorpage' 10 | 11 | export const QrCodeViewVpn: React.FC<{}> = () => { 12 | const history = useHistory() 13 | const qrCode = useSelector(nodeSelectors.getCurrentQrCode) 14 | const { t } = useTranslation() 15 | const { netid, clientid } = useParams<{ netid: string; clientid: string }>() 16 | const dispatch = useDispatch() 17 | 18 | useLinkBreadcrumb({ 19 | link: `/prouser/${netid}/vpnview`, 20 | title: clientid, 21 | }) 22 | 23 | useLinkBreadcrumb({ 24 | title: 'qr', 25 | }) 26 | 27 | const handleClose = () => { 28 | dispatch(clearQr()) 29 | history.goBack() 30 | } 31 | 32 | if (!qrCode) { 33 | return ( 34 | 35 | ) 36 | } 37 | 38 | const boxStyle = { 39 | modal: { 40 | position: 'absolute', 41 | display: 'flex', 42 | flexDirection: 'column', 43 | flex: 1, 44 | top: '50%', 45 | left: '50%', 46 | transform: 'translate(-50%, -50%)', 47 | width: 400, 48 | backgroundColor: 'white', 49 | border: '1px solid #000', 50 | minWidth: '33%', 51 | // boxShadow: 24, 52 | pt: 2, 53 | px: 4, 54 | pb: 3, 55 | }, 56 | center: { 57 | textAlign: 'center', 58 | }, 59 | max: { 60 | width: '75%', 61 | }, 62 | } as any 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | {`${t('extclient.qr')} : ${clientid}`} 71 | 72 | 73 | 74 | {`QR 79 | 80 | 81 | 82 | 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /src/proroute/proutils/ProConsts.tsx: -------------------------------------------------------------------------------- 1 | export const NET_ADMIN_ACCESS_LVL = 0 2 | export const NODE_ACCESS_LVL = 1 3 | export const CLIENT_ACCESS_LVL = 2 4 | -------------------------------------------------------------------------------- /src/proroute/proutils/ProNetworkSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { useHistory, useParams } from 'react-router-dom' 4 | import { proSelectors } from '~store/selectors' 5 | import { useTranslation } from 'react-i18next' 6 | import ProCustomSelect from './ProCustomSelect' 7 | import { FormControl, Grid, Typography } from '@mui/material' 8 | import { clearCurrentMetrics } from '~store/modules/server/actions' 9 | import { GenericError } from '~util/genericerror' 10 | 11 | import { authSelectors } from '~store/selectors' 12 | 13 | export const ProNetworkSelect: React.FC<{ 14 | selectAll?: boolean 15 | prepend?: string 16 | }> = ({ selectAll, prepend }) => { 17 | const networkNames = useSelector(proSelectors.getNetworkUserNetworks) 18 | const { t } = useTranslation() 19 | const history = useHistory() 20 | const { netid } = useParams<{ netid?: string }>() 21 | const dispatch = useDispatch() 22 | const user = useSelector(authSelectors.getUser) 23 | 24 | if (selectAll && !!netid) { 25 | networkNames.push(t('common.selectall')) 26 | } 27 | 28 | const titleStyle = { 29 | textAlign: 'center', 30 | } as any 31 | 32 | // if statement for if no network display message no networks 33 | if (networkNames.length === 0) { 34 | return 35 | } 36 | 37 | return ( 38 | 39 | 40 |
41 | 42 | {`${t('pro.label.welcome')} ${user?.name}`} 43 | 44 |
45 |
46 | 47 | 48 | { 51 | dispatch(clearCurrentMetrics()) 52 | const netIndex = history.location.pathname.indexOf(netid!) 53 | if (netid === undefined) { 54 | history.push(`${history.location.pathname}/${selected}`) 55 | } else if (selectAll && selected === t('common.selectall')) { 56 | history.push(history.location.pathname.substr(0, netIndex - 1)) 57 | } else if (netid !== undefined) { 58 | history.push( 59 | history.location.pathname.replace(netid!, selected) 60 | ) 61 | } 62 | }} 63 | items={networkNames} 64 | /> 65 | 66 | 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/route/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { Route, Redirect, RouteProps } from 'react-router-dom' 4 | import { User } from '~store/types' 5 | import { authSelectors } from '~store/selectors' 6 | import { LocationDescriptor } from 'history' 7 | 8 | interface PrivateRouteProps extends RouteProps { 9 | condition?: (user?: User) => boolean 10 | to?: LocationDescriptor 11 | } 12 | 13 | export const PrivateRoute: React.FC< 14 | React.PropsWithChildren 15 | > = ({ children, condition, to, ...rest }) => { 16 | const isLoggedIn = useSelector(authSelectors.getLoggedIn) 17 | const user = useSelector(authSelectors.getUser) 18 | const showRoute = condition ? condition(user) : isLoggedIn 19 | return ( 20 | { 23 | return showRoute ? ( 24 | children 25 | ) : ( 26 | 36 | ) 37 | }} 38 | /> 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/route/accesskeys/AccessKeys.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { AccessKeyCreate } from './components/AccessKeyCreate' 7 | import { AccessKeyView } from './components/AccessKeyView' 8 | import { NetworkSelect } from '~components/NetworkSelect' 9 | import { AccessKeyTable } from './components/AccessKeyTable' 10 | 11 | export const AccessKeys: React.FC = () => { 12 | const { path } = useRouteMatch() 13 | const { t } = useTranslation() 14 | 15 | useLinkBreadcrumb({ 16 | title: t('breadcrumbs.accessKeys'), 17 | }) 18 | 19 | const titleStyle = { 20 | textAlign: 'center', 21 | } as any 22 | 23 | return ( 24 | 25 | 26 | 27 | 33 | 34 |
35 | 36 | {t('accesskey.accesskeys')} 37 | 38 |
39 |
40 | 41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/route/accesskeys/components/AccessKeySelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { useRouteMatch, useHistory } from 'react-router-dom' 4 | import { networkSelectors } from '../../../store/selectors' 5 | import { useTranslation } from 'react-i18next' 6 | import CustomSelect from '../../../components/CustomSelect' 7 | import { Grid } from '@mui/material' 8 | 9 | export const AccessKeySelect: React.FC = () => { 10 | const networkNames = useSelector(networkSelectors.getNetworks).map( 11 | (net) => net.netid 12 | ) 13 | 14 | const { path } = useRouteMatch() 15 | const { t } = useTranslation() 16 | const history = useHistory() 17 | 18 | return ( 19 | 20 | 27 | { 30 | history.push(`${path}/${selected}`) 31 | }} 32 | items={networkNames} 33 | /> 34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/route/accesskeys/components/AccessKeyView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { networkSelectors } from '~store/types' 4 | import { useHistory, useParams } from 'react-router' 5 | import AccessKeyDetails from './AccessKeyDetails' 6 | import { useTranslation } from 'react-i18next' 7 | import { NotFound } from '~util/errorpage' 8 | 9 | export const AccessKeyView: React.FC<{}> = () => { 10 | const history = useHistory() 11 | const { t } = useTranslation() 12 | const { netid, keyname } = useParams<{ netid: string; keyname: string }>() 13 | const networks = useSelector(networkSelectors.getNetworks) 14 | const netIndex = networks.findIndex((network) => network.netid === netid) 15 | 16 | if (!~netIndex) { 17 | return 18 | } 19 | 20 | const accessKeys = networks[netIndex].accesskeys 21 | const keyIndex = accessKeys.findIndex( 22 | (accessKey) => accessKey.name === keyname 23 | ) 24 | const accessKeyInfo = accessKeys[keyIndex] 25 | 26 | const handleClose = () => history.goBack() 27 | 28 | return ( 29 | {}} 33 | handleClose={handleClose} 34 | accessString={accessKeyInfo.accessstring} 35 | keyValue={accessKeyInfo.value} 36 | netID={netid} 37 | /> 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/route/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import NetworkCard from '~components/dashboard/NetworkCard' 7 | import NodeCard from '~components/dashboard/NodeCard' 8 | import ExtClientsCard from '~components/dashboard/ExtClientsCard' 9 | import DNSCard from '~components/dashboard/DNSCard' 10 | import UserCard from '~components/dashboard/UserCard' 11 | import AdminCard from '~components/dashboard/AdminCard' 12 | import { useSelector } from 'react-redux' 13 | import { authSelectors } from '~store/types' 14 | import ACLCard from '~components/dashboard/ACLCard' 15 | import GraphCard from '~components/dashboard/GraphCard' 16 | import './dashboard-styles.css' 17 | import EnrollmentKeyCard from '~components/dashboard/EnrollmentKeyCard' 18 | 19 | export const Dashboard: React.FC = () => { 20 | const { path } = useRouteMatch() 21 | const { t } = useTranslation() 22 | const user = useSelector(authSelectors.getUser) 23 | 24 | useLinkBreadcrumb({ 25 | title: t('breadcrumbs.dashboard'), 26 | }) 27 | 28 | return ( 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {user?.isAdmin && ( 54 | <> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | )} 69 | 70 | 71 | 72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/route/dashboard/dashboard-styles.css: -------------------------------------------------------------------------------- 1 | .clickable:hover { 2 | transform: scale(1.05, 1.05); 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /src/route/dns/DNS.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { NetworkSelect } from '~components/NetworkSelect' 7 | import { useSelector } from 'react-redux' 8 | import { serverSelectors } from '~store/types' 9 | import { DNSView } from './components/DNSView' 10 | import { DNSEntryCreate } from './components/DNSCreate' 11 | 12 | export const DNS: React.FC = () => { 13 | const { path } = useRouteMatch() 14 | const { t } = useTranslation() 15 | const hasDNS = useSelector(serverSelectors.getServerConfig).DNSMode 16 | 17 | useLinkBreadcrumb({ 18 | title: t('breadcrumbs.dns'), 19 | }) 20 | 21 | const titleStyle = { 22 | textAlign: 'center', 23 | } as any 24 | 25 | const DnsTitle = () => ( 26 | 27 | 28 |
29 | 30 | {hasDNS ? t('dns.title') : t('dns.disabled')} 31 | 32 |
33 |
34 |
35 | ) 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | {hasDNS && } 43 | 44 | 45 | {!hasDNS ? : } 46 | 47 | 48 | {!hasDNS ? : } 49 | 50 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/route/dns/components/DNSSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import CustomSelect from '../../../components/CustomSelect' 4 | import { Grid } from '@mui/material' 5 | 6 | export const DNSSelect: React.FC<{ 7 | nodeAddrs: Array 8 | onSelect: (selected: string) => void 9 | }> = ({ nodeAddrs, onSelect }) => { 10 | const { t } = useTranslation() 11 | 12 | return ( 13 | 14 | 21 | { 24 | onSelect(selected.split(' ')[0]) 25 | }} 26 | items={nodeAddrs} 27 | /> 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/route/extclients/ExtClients.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | // import { NetworkCreate } from './create/NetworkCreate' 5 | import { useTranslation } from 'react-i18next' 6 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 7 | import { NetworkSelect } from './components/NetworkSelect' 8 | import { ExtClientView } from './components/ExtClientView' 9 | import { QrCodeView } from './components/QrCodeView' 10 | import { ExtClientEdit } from './components/ExtClientEdit' 11 | 12 | export const ExtClients: React.FC = () => { 13 | const { path } = useRouteMatch() 14 | const { t } = useTranslation() 15 | 16 | useLinkBreadcrumb({ 17 | title: t('breadcrumbs.extClients'), 18 | }) 19 | 20 | const titleStyle = { 21 | textAlign: 'center', 22 | } as any 23 | 24 | return ( 25 | 26 | 27 | 28 | 34 | 35 |
36 | 37 | {t('extclient.extclients')} 38 | 39 |
40 |
41 | 42 | 43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/route/extclients/components/DeleteExtClientButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Delete } from '@mui/icons-material' 3 | import { IconButton, Tooltip } from '@mui/material' 4 | import { useDispatch } from 'react-redux' 5 | import { i18n } from '../../../i18n/i18n' 6 | import { deleteExternalClient } from '~store/modules/node/actions' 7 | import { ExternalClient } from '~store/types' 8 | import CustomizedDialogs from '~components/dialog/CustomDialog' 9 | 10 | export const DeleteExtClientButton: React.FC<{ 11 | client: ExternalClient 12 | }> = ({ client }) => { 13 | const dispatch = useDispatch() 14 | const [open, setOpen] = React.useState(false) 15 | 16 | const handleDeleteClient = () => { 17 | dispatch( 18 | deleteExternalClient.request({ 19 | clientName: client.clientid, 20 | netid: client.network, 21 | }) 22 | ) 23 | } 24 | 25 | const handleClose = () => setOpen(false) 26 | 27 | const handleOpen = () => setOpen(true) 28 | 29 | return ( 30 | <> 31 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/route/extclients/components/DownloadExtClientButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Download, QrCode2 } from '@mui/icons-material' 3 | import { IconButton, Tooltip } from '@mui/material' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import { i18n } from '../../../i18n/i18n' 6 | import { getExternalClientConf } from '~store/modules/node/actions' 7 | import { authSelectors, ExternalClient } from '~store/types' 8 | import { useHistory, useRouteMatch } from 'react-router' 9 | 10 | export const DownloadExtClientButton: React.FC<{ 11 | client: ExternalClient 12 | type: 'qr' | 'file' 13 | }> = ({ client, type }) => { 14 | const dispatch = useDispatch() 15 | const token = useSelector(authSelectors.getToken) 16 | const history = useHistory() 17 | const { url } = useRouteMatch() 18 | 19 | const handleDownloadConf = () => { 20 | dispatch( 21 | getExternalClientConf.request({ 22 | clientid: client.clientid, 23 | netid: client.network, 24 | type, 25 | token: token || '', 26 | }) 27 | ) 28 | 29 | if (type === 'qr') { 30 | history.push(`${url}/${client.clientid}/qr`) 31 | } 32 | } 33 | 34 | return ( 35 | 43 | 44 | {type === 'file' ? : } 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/route/extclients/components/EditExtClientButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tooltip } from '@mui/material' 3 | import { i18n } from '../../../i18n/i18n' 4 | import { ExternalClient } from '~store/types' 5 | import { useRouteMatch } from 'react-router' 6 | import { NmLink } from '~components/Link' 7 | 8 | export const EditExtClientButton: React.FC<{ 9 | client: ExternalClient 10 | }> = ({ client }) => { 11 | const { url } = useRouteMatch() 12 | 13 | return ( 14 | 18 | 19 | {client.clientid} 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/route/extclients/components/ExtClientCreateButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AddCircleOutline } from '@mui/icons-material' 3 | import { IconButton, Tooltip } from '@mui/material' 4 | import { useDispatch } from 'react-redux' 5 | import { i18n } from '../../../i18n/i18n' 6 | import { createExternalClient } from '~store/modules/node/actions' 7 | import { Node } from '~store/types' 8 | 9 | export const ExtClientCreateButton: React.FC<{ 10 | node: Node 11 | }> = ({ node }) => { 12 | const dispatch = useDispatch() 13 | 14 | const handleExtClientCreate = () => { 15 | dispatch( 16 | createExternalClient.request({ 17 | netid: node.network, 18 | nodeid: node.id, 19 | }) 20 | ) 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/route/extclients/components/NetClientsView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { useRouteMatch, useHistory } from 'react-router-dom' 4 | import { networkSelectors } from '../../../store/selectors' 5 | import { useTranslation } from 'react-i18next' 6 | import CustomSelect from '../../../components/CustomSelect' 7 | import { Grid } from '@mui/material' 8 | 9 | export const NetClientsView: React.FC = () => { 10 | const listOfNetworks = useSelector(networkSelectors.getNetworks) 11 | const networkNames = [] 12 | if (listOfNetworks) { 13 | for (let i = 0; i < listOfNetworks.length; i++) { 14 | networkNames.push(listOfNetworks[i].netid) 15 | } 16 | } 17 | const { path } = useRouteMatch() 18 | const { t } = useTranslation() 19 | const history = useHistory() 20 | 21 | const centerStyle = { 22 | textAlign: 'center', 23 | } as any 24 | 25 | return ( 26 | 27 | 28 | { 31 | history.push(`${path}/${selected}`) 32 | }} 33 | items={networkNames} 34 | /> 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/route/extclients/components/NetworkSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { useRouteMatch, useHistory } from 'react-router-dom' 4 | import { networkSelectors } from '../../../store/selectors' 5 | import { useTranslation } from 'react-i18next' 6 | import CustomSelect from '../../../components/CustomSelect' 7 | import { Grid } from '@mui/material' 8 | 9 | export const NetworkSelect: React.FC = () => { 10 | const listOfNetworks = useSelector(networkSelectors.getNetworks) 11 | const networkNames = [] 12 | if (listOfNetworks) { 13 | for (let i = 0; i < listOfNetworks.length; i++) { 14 | networkNames.push(listOfNetworks[i].netid) 15 | } 16 | } 17 | const { path } = useRouteMatch() 18 | const { t } = useTranslation() 19 | const history = useHistory() 20 | 21 | const centerStyle = { 22 | textAlign: 'center', 23 | } as any 24 | 25 | return ( 26 | 27 | 28 | { 31 | history.push(`${path}/${selected}`) 32 | }} 33 | items={networkNames} 34 | /> 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/route/extclients/components/QrCodeView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Grid, Modal, Typography, Box } from '@mui/material' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { useTranslation } from 'react-i18next' 5 | import { useHistory, useRouteMatch, useParams } from 'react-router' 6 | import { nodeSelectors } from '~store/types' 7 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 8 | import { clearQr } from '~store/modules/node/actions' 9 | import { NotFound } from '~util/errorpage' 10 | 11 | export const QrCodeView: React.FC<{}> = () => { 12 | const history = useHistory() 13 | const qrCode = useSelector(nodeSelectors.getCurrentQrCode) 14 | const { t } = useTranslation() 15 | const { path } = useRouteMatch() 16 | const { netid, clientid } = useParams<{ netid: string; clientid: string }>() 17 | const newPath = `${path.split(':netid')[0]}${netid}` 18 | const dispatch = useDispatch() 19 | 20 | useLinkBreadcrumb({ 21 | link: newPath, 22 | title: clientid, 23 | }) 24 | 25 | useLinkBreadcrumb({ 26 | title: 'qr', 27 | }) 28 | 29 | const handleClose = () => { 30 | dispatch(clearQr()) 31 | history.push(newPath) 32 | } 33 | 34 | if (!qrCode) { 35 | return 36 | } 37 | 38 | const boxStyle = { 39 | modal: { 40 | position: 'absolute', 41 | display: 'flex', 42 | flexDirection: 'column', 43 | flex: 1, 44 | top: '50%', 45 | left: '50%', 46 | transform: 'translate(-50%, -50%)', 47 | width: 400, 48 | backgroundColor: 'white', 49 | border: '1px solid #000', 50 | minWidth: '33%', 51 | // boxShadow: 24, 52 | pt: 2, 53 | px: 4, 54 | pb: 3, 55 | }, 56 | center: { 57 | textAlign: 'center', 58 | }, 59 | max: { 60 | width: '75%', 61 | }, 62 | } as any 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | {`${t('extclient.qr')} : ${clientid}`} 71 | 72 | 73 | 74 | {`QR 79 | 80 | 81 | 82 | 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /src/route/graph/Graphs.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { NetworkSelect } from '~components/NetworkSelect' 7 | // import { NetworkGraph } from './components/NetworkGraph' 8 | import { NetworkGraph } from './components/NetworkGraph' 9 | 10 | export const Graphs: React.FC = () => { 11 | const { path } = useRouteMatch() 12 | const { t } = useTranslation() 13 | 14 | useLinkBreadcrumb({ 15 | title: t('breadcrumbs.graphs'), 16 | }) 17 | 18 | const titleStyle = { 19 | textAlign: 'center', 20 | } as any 21 | 22 | return ( 23 | 24 | 25 | 26 | 32 | 33 |
34 | 35 | {t('network.graphs')} 36 | 37 |
38 |
39 |
40 | 41 |
42 | 43 | 44 | 45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/route/graph/components/graph-components/types.tsx: -------------------------------------------------------------------------------- 1 | import { NodeConnectivityStatus } from "~store/types" 2 | 3 | // interfaces and types for use in graph-components 4 | export interface DataNode { 5 | type: DataNodeType 6 | id: string 7 | name: string 8 | lastCheckin: number | undefined 9 | address?: string 10 | address6?: string 11 | } 12 | 13 | export interface AltDataNode { 14 | id: string 15 | name: string 16 | type: 'extclient' | 'cidr' 17 | address?: string 18 | address6?: string 19 | } 20 | 21 | export interface Edge { 22 | from: string 23 | to: string 24 | status: NodeConnectivityStatus 25 | } 26 | 27 | export type DataNodeType = 'normal' | '1&e' | 'ingress' | 'egress' | 'i&r' | 'e&r' | 'relay' | 'relayed' | 'extclient' | 'cidr' | 'i&e&r' 28 | -------------------------------------------------------------------------------- /src/route/graph/components/graph-components/util.tsx: -------------------------------------------------------------------------------- 1 | import { DataNodeType } from "./types" 2 | import { isNodeHealthy } from "~util/fields" 3 | import { NodeConnectivityStatus } from "~store/types" 4 | 5 | // util functions for graph nodes 6 | export const getNodeColor = (dataType: DataNodeType, lastcheckin: number | undefined) => { 7 | if (!!lastcheckin) { 8 | switch(isNodeHealthy(lastcheckin)) { 9 | case 1: return "#ff9800" 10 | case 2: return "#f44336" 11 | } 12 | } 13 | switch(dataType) { 14 | case 'normal': return "#2b00ff" 15 | case '1&e': return '#d9ffa3' 16 | case 'e&r': return '#19ffb2' 17 | case 'i&r': return '#d5db8a' 18 | case 'egress': return '#6bdbb6' 19 | case 'ingress': return '#ebde34' 20 | case 'extclient': return '#26ffff' 21 | case 'relay': return '#a552ff' 22 | case 'relayed': return '#639cbf' 23 | case 'cidr': return '#6fa397' 24 | case 'i&e&r': return '#f2c7ff' 25 | default: return "#2b00ff" 26 | } 27 | } 28 | 29 | /** 30 | * Get edge color based on edge connectivity status. 31 | * 32 | * @param {NodeConnectivityStatus} status status of the edge 33 | * @returns hex color code 34 | */ 35 | export const getEdgeColor = (status: NodeConnectivityStatus): string => { 36 | const RED_COLOR = '#CC0000' 37 | const YELLOW_COLOR = '#CCCC00' 38 | const GREEN_COLOR = '#00CC00' 39 | const WHITE_COLOR = '#FFFFFF' 40 | 41 | switch (status) { 42 | case 'healthy': 43 | return GREEN_COLOR 44 | case 'warning': 45 | return YELLOW_COLOR 46 | case 'error': 47 | return RED_COLOR 48 | default: 49 | return WHITE_COLOR 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/route/hosts/HostsPage.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useEffect } from 'react' 2 | import { Container, Grid, Typography } from '@mui/material' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { HostDetailPage } from './HostDetailPage' 7 | import { HostsTable } from './components/HostsTable' 8 | import { getHosts, updateHost } from '~store/modules/hosts/actions' 9 | import { useDispatch, useSelector } from 'react-redux' 10 | import { hostsSelectors } from '~store/selectors' 11 | import { Host } from '~store/types' 12 | 13 | export const HostsPage: FC = () => { 14 | const { path } = useRouteMatch() 15 | const { t } = useTranslation() 16 | const dispatch = useDispatch() 17 | const hosts = useSelector(hostsSelectors.getHosts) 18 | 19 | useLinkBreadcrumb({ 20 | title: t('breadcrumbs.hosts'), 21 | }) 22 | 23 | const titleStyle = { 24 | textAlign: 'center', 25 | marginBottom: '2rem', 26 | } as any 27 | 28 | const refreshHosts = useCallback(() => { 29 | dispatch(getHosts.request()) 30 | }, [dispatch]) 31 | 32 | const onToggleDefaultness = useCallback((host: Host) => { 33 | dispatch(updateHost.request({ ...host, isdefault: !host.isdefault })) 34 | }, [dispatch]) 35 | 36 | // on created 37 | useEffect(() => { 38 | refreshHosts() 39 | }, [refreshHosts]) 40 | 41 | return ( 42 | 43 | 44 | {/* all hosts page */} 45 | 46 | 52 | 53 |
54 | {t('hosts.hosts')} 55 |
56 |
57 | 61 |
62 |
63 | 64 | {/* host details page */} 65 | 66 | 67 | 68 |
69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/route/login/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { Modal, Box, useTheme } from '@mui/material' 4 | import { useHistory } from 'react-router-dom' 5 | import { authSelectors } from '../../store/selectors' 6 | import ProLogin from '../../proroute/prologin/Login' 7 | import CreateAdmin from '../../proroute/prologin/CreateAdmin' 8 | 9 | const styles = { 10 | centerText: { 11 | textAlign: 'center', 12 | }, 13 | vertTabs: { 14 | flex: 1, 15 | display: 'flex', 16 | flexDirection: 'column', 17 | position: 'relative', 18 | }, 19 | mainContainer: { 20 | marginTop: '2em', 21 | display: 'flex', 22 | justifyContent: 'center', 23 | alignItems: 'center', 24 | }, 25 | center: { 26 | flex: 1, 27 | display: 'flex', 28 | textAlign: 'center', 29 | }, 30 | modal: { 31 | position: 'absolute', 32 | display: 'flex', 33 | flexDirection: 'column', 34 | flex: 1, 35 | top: '50%', 36 | left: '50%', 37 | transform: 'translate(-50%, -50%)', 38 | width: 400, 39 | backgroundColor: 'white', 40 | border: '1px solid #000', 41 | minWidth: '33%', 42 | // boxShadow: 24, 43 | pt: 2, 44 | px: 4, 45 | pb: 3, 46 | }, 47 | } as any 48 | 49 | export function Login() { 50 | const history = useHistory() 51 | const hasAdmin = useSelector(authSelectors.hasAdmin) 52 | const theme = useTheme() 53 | 54 | const authRenderSwitch = () => { 55 | if (hasAdmin) return 56 | return 57 | } 58 | 59 | return ( 60 | { 64 | const ignorableReasons = ['escapeKeyDown', 'backdropClick'] 65 | if (ignorableReasons.includes(reason)) { 66 | return 67 | } 68 | history.goBack() 69 | }} 70 | > 71 | 77 | {authRenderSwitch()} 78 | 79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/route/networks/networkId/components/NetworkModifiedStats.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, TextField } from '@mui/material' 2 | import { useTranslation } from 'react-i18next' 3 | import { datePickerConverter } from '~util/unixTime' 4 | import { useNetwork } from '~util/network' 5 | 6 | export const NetworkModifiedStats: React.FC<{ netid: string }> = ({ 7 | netid, 8 | }) => { 9 | const network = useNetwork(netid) 10 | const { t } = useTranslation() 11 | 12 | const fieldStyle = { 13 | marginTop: '1em', 14 | } 15 | 16 | if (!network) { 17 | return null 18 | } 19 | return ( 20 | 27 | 28 | 38 | 39 | 40 | 41 | 51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/route/networkusers/NetworkUsers.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { NetworkSelect } from '~components/NetworkSelect' 7 | import { NetworkUsersTable } from './components/NetworkUsersTable' 8 | import { NetworkUserEdit } from './components/NetworkUserEdit' 9 | 10 | export const NetworkUsers: React.FC = () => { 11 | const { path } = useRouteMatch() 12 | const { t } = useTranslation() 13 | 14 | useLinkBreadcrumb({ 15 | title: t('pro.label.userpermissions'), 16 | }) 17 | 18 | const titleStyle = { 19 | textAlign: 'center', 20 | } as any 21 | 22 | return ( 23 | 24 | 25 | 26 | 32 | 33 |
34 | 35 | {t('pro.label.userpermissions')} 36 | 37 |
38 |
39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/route/node_acls/NodeACLs.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { NetworkSelect } from '~components/NetworkSelect' 7 | import { NodesACLTable } from './components/NodesACLTable' 8 | 9 | export const NodeAcls: React.FC = () => { 10 | const { path, url } = useRouteMatch() 11 | const { t } = useTranslation() 12 | 13 | useLinkBreadcrumb({ 14 | link: url, 15 | title: t('breadcrumbs.acls'), 16 | }) 17 | 18 | const titleStyle = { 19 | textAlign: 'center', 20 | } as any 21 | 22 | return ( 23 | 24 | 25 | 26 | 32 | 33 |
34 | 35 | {t('acls.nodes')} 36 | 37 |
38 |
39 |
40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/route/nodes/netid/components/HubButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { useTranslation } from 'react-i18next' 4 | import { IconButton, Tooltip } from '@mui/material' 5 | import { updateNode } from '~modules/node/actions' 6 | import { hostsSelectors, Node } from '~store/types' 7 | import CustomDialog from '~components/dialog/CustomDialog' 8 | 9 | const hoverBlueStyle = { 10 | ':hover': { 11 | color: '#3f51b5', 12 | }, 13 | } 14 | 15 | export const HubButton: React.FC<{ 16 | node: Node 17 | createText: string 18 | disabledText: string 19 | SignalIcon: ReactNode 20 | children?: ReactNode 21 | extraLogic?: () => void 22 | }> = ({ node, createText, disabledText, SignalIcon, extraLogic }) => { 23 | const dispatch = useDispatch() 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | const [hovering, setHovering] = React.useState(false) 26 | const [open, setOpen] = React.useState(false) 27 | const { t } = useTranslation() 28 | const hostsMap = useSelector(hostsSelectors.getHostsMap) 29 | const handleHoverEnter = () => setHovering(true) 30 | const handleHoverLeave = () => setHovering(false) 31 | 32 | const handleOpen = () => setOpen(true) 33 | const handleClose = () => setOpen(false) 34 | 35 | const createHub = () => { 36 | dispatch( 37 | updateNode.request({ 38 | netid: node.network, 39 | token: '', 40 | node: { 41 | ...node, 42 | }, 43 | }) 44 | ) 45 | if (!!extraLogic) { 46 | extraLogic() 47 | } 48 | handleClose() 49 | } 50 | 51 | return ( 52 | <> 53 | 60 | 61 | 62 | 69 | {SignalIcon} 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/route/usergroups/UserGroups.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Typography } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { NmLink } from '~components/index' 7 | import { UserGroupsTable } from './table/UserGroupsTable' 8 | import { UserGroupCreate } from './create/UserGroupCreate' 9 | 10 | export const UserGroups: React.FC = () => { 11 | const { path } = useRouteMatch() 12 | const { t } = useTranslation() 13 | 14 | useLinkBreadcrumb({ 15 | title: t('pro.label.usergroups'), 16 | }) 17 | 18 | return ( 19 | 20 | 21 | 22 | 29 | 30 | {t('pro.label.usergroups')} 31 | 32 | 33 | 34 | {`${t('common.create')} ${t('pro.label.usergroup')}`} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/route/usergroups/table/UserGroupsTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { proSelectors } from '~store/selectors' 4 | import { NmTable, TableColumns } from '~components/Table' 5 | import { Delete } from '@mui/icons-material' 6 | import { useDialog } from '~components/ConfirmDialog' 7 | import { useTranslation } from 'react-i18next' 8 | import { deleteUserGroup, getUserGroups } from '~store/modules/pro/actions' 9 | import { IconButton, Tooltip } from '@mui/material' 10 | 11 | type UserGroup = { 12 | groupname: string; 13 | action?: any; 14 | } 15 | 16 | export const UserGroupsTable: React.FC = () => { 17 | const userGroups = useSelector(proSelectors.userGroups) 18 | const { Component: Dialog, setProps } = useDialog() 19 | const { t } = useTranslation() 20 | const dispatch = useDispatch() 21 | const currentGroups = [] as UserGroup[] 22 | 23 | if (!!userGroups && !!userGroups.length) { 24 | for (let i = 0; i < userGroups.length; i++) { 25 | currentGroups.push({groupname: userGroups[i]}) 26 | } 27 | } 28 | 29 | React.useEffect(() => { 30 | if (!!!userGroups || !!!userGroups.length) { 31 | dispatch(getUserGroups.request()) 32 | } 33 | }, [userGroups, dispatch]) 34 | 35 | const columns: TableColumns = [ 36 | { 37 | id: 'groupname', 38 | labelKey: 'pro.label.usergroup', 39 | minWidth: 200, 40 | sortable: true, 41 | align: 'center', 42 | }, 43 | { 44 | id: 'action', 45 | labelKey: 'common.delete', 46 | minWidth: 100, 47 | align: 'center', 48 | format: ((_, row) => 49 | { 52 | setProps({ 53 | message: `${t('common.delete')} ${t('pro.label.usergroup')} "${row.groupname}"`, 54 | title: t('common.submit'), 55 | onSubmit: () => { 56 | dispatch( 57 | deleteUserGroup.request({ 58 | groupName: row.groupname 59 | }) 60 | )}, 61 | }) 62 | }} > 63 | 64 | 65 | ) 66 | }, 67 | ] 68 | 69 | return ( 70 | <> 71 | `${row.groupname}-${i}`} 75 | /> 76 | 77 | 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/route/users/UsersCommunity.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid } from '@mui/material' 2 | import React from 'react' 3 | import { useRouteMatch, Switch, Route } from 'react-router-dom' 4 | import { useTranslation } from 'react-i18next' 5 | import { useLinkBreadcrumb } from '~components/PathBreadcrumbs' 6 | import { NmLink } from '~components/index' 7 | import { UserTableCommunity } from './components/UserTableCommunity' 8 | import { UserCreate } from './create/UserCreate' 9 | import { UserEditCommunity } from './userId/UserEditCommunity' 10 | import { UserChangePassword } from './userId/UserChangePassword' 11 | 12 | export const UsersCommunity: React.FC = () => { 13 | const { path } = useRouteMatch() 14 | const { t } = useTranslation() 15 | 16 | useLinkBreadcrumb({ 17 | title: t('breadcrumbs.users'), 18 | }) 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 32 | 33 |

{t('users.header')}

34 |
35 | 36 | 37 | {t('users.create.button')} 38 | 39 | 40 |
41 |
42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/route/users/components/UserTableCommunity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { authSelectors } from '~store/selectors' 4 | import { NmLink } from '~components/Link' 5 | import { NmTable, TableColumns } from '~components/Table' 6 | import { User } from '~store/types' 7 | import { Delete } from '@mui/icons-material' 8 | import { useDialog } from '~components/ConfirmDialog' 9 | import { useTranslation } from 'react-i18next' 10 | import { deleteUser } from '~store/modules/auth/actions' 11 | 12 | const columns: TableColumns = [ 13 | { 14 | id: 'name', 15 | labelKey: 'users.table.name', 16 | minWidth: 170, 17 | sortable: true, 18 | format: (username) => {username}, 19 | }, 20 | { 21 | id: 'isAdmin', 22 | labelKey: 'users.table.isAdmin', 23 | minWidth: 100, 24 | sortable: true, 25 | format: (isAdmin) => (isAdmin ? 'True' : 'False'), 26 | }, 27 | { 28 | id: 'networks', 29 | labelKey: 'users.table.networks', 30 | minWidth: 150, 31 | sortable: false, 32 | format: (networks, user) => { 33 | if (user.isAdmin && (!networks || !networks.length)) return * 34 | return ( 35 | 36 | {networks?.map((network) => ( 37 | 38 | {network} 39 | 40 | ))} 41 | 42 | ) 43 | }, 44 | }, 45 | ] 46 | 47 | export const UserTableCommunity: React.FC = () => { 48 | const users = useSelector(authSelectors.getUsers) 49 | const { Component: Dialog, setProps } = useDialog() 50 | const { t } = useTranslation() 51 | const dispatch = useDispatch() 52 | 53 | return ( 54 | <> 55 | row.name} 59 | actions={[ 60 | (row) => ({ 61 | tooltip: t('common.delete'), 62 | disabled: false, 63 | icon: , 64 | onClick: () => { 65 | setProps({ 66 | message: t('users.delete'), 67 | title: t('users.deleteTitle'), 68 | onSubmit: () => 69 | dispatch( 70 | deleteUser.request({ 71 | username: row.name, 72 | }) 73 | ), 74 | }) 75 | }, 76 | }), 77 | ]} 78 | /> 79 | 80 | 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect' 6 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from 'typesafe-actions' 2 | 3 | import { actions as auth } from './modules/auth' 4 | import { actions as network } from './modules/network' 5 | import { actions as node } from './modules/node' 6 | import { actions as server } from './modules/server' 7 | import { actions as toast } from './modules/toast' 8 | import { actions as router } from './modules/router' 9 | import { actions as acls } from './modules/acls' 10 | import { actions as pro } from './modules/pro' 11 | import { actions as hosts } from './modules/hosts' 12 | import { actions as enrollmentKeys } from './modules/enrollmentkeys' 13 | 14 | export const actions = { 15 | auth, 16 | network, 17 | node, 18 | server, 19 | toast, 20 | router, 21 | acls, 22 | pro, 23 | hosts, 24 | enrollmentKeys, 25 | } 26 | 27 | export type RootAction = ActionType 28 | -------------------------------------------------------------------------------- /src/store/createStore.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useSelector } from 'react-redux' 2 | import { applyMiddleware, createStore } from 'redux' 3 | import { createLogger } from 'redux-logger' 4 | import createSagaMiddleware from 'redux-saga' 5 | import { DEBUG } from '../config' 6 | 7 | import { RootAction as RA } from './actions' 8 | import { createRootReducer, RootState as RS } from './reducers' 9 | import { createRootSaga } from './sagas' 10 | 11 | declare module 'typesafe-actions' { 12 | export type RootAction = RA 13 | export type RootState = RS 14 | interface Types { 15 | RootAction: RootAction 16 | RootState: RootState 17 | } 18 | } 19 | 20 | const logger = createLogger({ 21 | level: 'info', 22 | }) 23 | 24 | export const createReduxStore = () => { 25 | const sagaMiddleware = createSagaMiddleware() 26 | let enhancer 27 | if (DEBUG) { 28 | enhancer = applyMiddleware(logger, sagaMiddleware) 29 | } else { 30 | enhancer = applyMiddleware(sagaMiddleware) 31 | } 32 | const store = createStore(createRootReducer(), enhancer) 33 | sagaMiddleware.run(createRootSaga()) 34 | return store 35 | } 36 | 37 | export const useMintableSelector: TypedUseSelectorHook = useSelector 38 | -------------------------------------------------------------------------------- /src/store/modules/acls/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, createAsyncAction } from 'typesafe-actions' 2 | import { GetACLContainerPayload, UpdateNodeACLContainerPayload, UpdateNodeACLPayload } from '.' 3 | 4 | export const getNodeACLContainer = createAsyncAction( 5 | 'ACLs_getNodeACLContainer_Request', 6 | 'ACLs_getNodeACLContainer_Success', 7 | 'ACLs_getNodeACLContainer_Failure' 8 | )() 9 | 10 | export const updateNodeContainerACL = createAsyncAction( 11 | 'ACLs_updateNodeACLContainer_Request', 12 | 'ACLs_updateNodeACLContainer_Success', 13 | 'ACLs_updateNodeACLContainer_Failure' 14 | )() 15 | 16 | export const updateNodeACL = createAsyncAction( 17 | 'ACLs_updateNodeACL_Request', 18 | 'ACLs_updateNodeACL_Success', 19 | 'ACLs_updateNodeACL_Failure' 20 | )() 21 | 22 | export const clearCurrentACL = createAction('clearCurrentACLContainer')() 23 | -------------------------------------------------------------------------------- /src/store/modules/acls/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/acls/reducer.ts: -------------------------------------------------------------------------------- 1 | import { produce } from 'immer' 2 | import { createReducer } from 'typesafe-actions' 3 | import { 4 | updateNodeContainerACL, 5 | getNodeACLContainer, 6 | clearCurrentACL, 7 | } from './actions' 8 | import { 9 | NodeACLContainer 10 | } from './types' 11 | 12 | export const reducer = createReducer({ 13 | isProcessing: false as boolean, 14 | currentACL: {} as NodeACLContainer, 15 | }) 16 | .handleAction(clearCurrentACL, (state, _) => 17 | produce(state, (draftState) => { 18 | draftState.isProcessing = false 19 | draftState.currentACL = {} 20 | }) 21 | ) 22 | .handleAction(getNodeACLContainer['request'], (state, _) => 23 | produce(state, (draftState) => { 24 | draftState.isProcessing = true 25 | }) 26 | ) 27 | .handleAction(getNodeACLContainer['success'], (state, payload) => 28 | produce(state, (draftState) => { 29 | draftState.isProcessing = false 30 | draftState.currentACL = payload.payload 31 | }) 32 | ) 33 | .handleAction(getNodeACLContainer['failure'], (state, _) => 34 | produce(state, (draftState) => { 35 | draftState.isProcessing = false 36 | }) 37 | ) 38 | .handleAction(updateNodeContainerACL['request'], (state, _) => 39 | produce(state, (draftState) => { 40 | draftState.isProcessing = true 41 | }) 42 | ) 43 | .handleAction(updateNodeContainerACL['success'], (state, payload) => 44 | produce(state, (draftState) => { 45 | draftState.isProcessing = false 46 | draftState.currentACL = payload.payload 47 | }) 48 | ) 49 | .handleAction(updateNodeContainerACL['failure'], (state, _) => 50 | produce(state, (draftState) => { 51 | draftState.isProcessing = false 52 | }) 53 | ) 54 | // .handleAction(updateNodeACL['request'], (state, _) => 55 | // produce(state, (draftState) => { 56 | // draftState.isProcessing = true 57 | // }) 58 | // ) 59 | // .handleAction(updateNodeACL['success'], (state, payload) => 60 | // produce(state, (draftState) => { 61 | // draftState.isProcessing = false 62 | // const { nodeID, nodeACL } = payload.payload 63 | // draftState.currentACL[nodeID] = nodeACL 64 | // }) 65 | // ) 66 | // .handleAction(updateNodeACL['failure'], (state, _) => 67 | // produce(state, (draftState) => { 68 | // draftState.isProcessing = false 69 | // }) 70 | // ) 71 | -------------------------------------------------------------------------------- /src/store/modules/acls/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getACLState = (state: RootState) => state.acls 5 | 6 | export const isProcessing = createSelector( 7 | getACLState, 8 | (acl) => acl.isProcessing 9 | ) 10 | export const getCurrentACL = createSelector(getACLState, (acl) => acl.currentACL) 11 | -------------------------------------------------------------------------------- /src/store/modules/acls/types.ts: -------------------------------------------------------------------------------- 1 | export type NodeID = string 2 | 3 | export type NodeACL = Record 4 | 5 | export type MutableRequired = { -readonly [P in keyof T]-?: T[P] }; 6 | 7 | export type NodeACLContainer = MutableRequired<{ [nodeID: NodeID] : NodeACL}> 8 | 9 | export interface UpdateNodeACLPayload { 10 | Request: { 11 | netid: string 12 | nodeid: NodeID 13 | nodeACL: NodeACL 14 | } 15 | Response: { 16 | nodeID: NodeID 17 | nodeACL: NodeACL 18 | } 19 | } 20 | 21 | export interface UpdateNodeACLContainerPayload { 22 | Request: { 23 | netid: string 24 | aclContainer: NodeACLContainer 25 | } 26 | Response: NodeACLContainer 27 | } 28 | 29 | export interface GetACLContainerPayload { 30 | Request: { 31 | netid: string 32 | } 33 | Response: NodeACLContainer 34 | } 35 | -------------------------------------------------------------------------------- /src/store/modules/api/index.ts: -------------------------------------------------------------------------------- 1 | export { reducer } from './reducer' 2 | export { saga } from './saga' 3 | export * as selectors from './selectors' 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /src/store/modules/api/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'typesafe-actions' 2 | import axios from 'axios' 3 | import { BACKEND_URL } from '../../../config' 4 | 5 | export const reducer = createReducer({ 6 | axios: axios.create({ 7 | baseURL: `${BACKEND_URL}/api`, 8 | }), 9 | }) 10 | -------------------------------------------------------------------------------- /src/store/modules/api/saga.ts: -------------------------------------------------------------------------------- 1 | import { select, all, call, put } from 'redux-saga/effects' 2 | import { produce } from 'immer' 3 | import { getToken, getLoggedIn } from '../auth/selectors' 4 | import { getApi } from './selectors' 5 | import { AxiosResponse, AxiosInstance, AxiosRequestConfig } from 'axios' 6 | import { KeysOfAxios, ApiRequestSignatureGenerator } from './types' 7 | import { logout } from '../auth/actions' 8 | import { i18n } from '../../../i18n/i18n' 9 | 10 | export function* apiRequestSaga( 11 | method: K, 12 | ...params: Required> 13 | ) { 14 | const api: ReturnType = yield select(getApi) 15 | return (yield call( 16 | api[method], 17 | ...(params as unknown as Parameters) 18 | )) as AxiosResponse 19 | } 20 | 21 | export function* apiRequestWithAuthSaga( 22 | method: K, 23 | ...params: Required> 24 | ) { 25 | const token: ReturnType = yield select(getToken) 26 | const isLoggedIn: boolean = yield select(getLoggedIn) 27 | if(!isLoggedIn && !!token) { 28 | yield put(logout()) 29 | throw new Error(i18n.t('error.tokenexpire')) 30 | } 31 | let config = params.pop() as AxiosRequestConfig 32 | config = produce(config, (draft) => { 33 | draft.headers = draft.headers ? draft.headers : {} 34 | draft.headers['authorization'] = `Bearer ${token}` 35 | }) 36 | params.push(config) 37 | return (yield call>( 38 | apiRequestSaga, 39 | method, 40 | ...params 41 | )) as AxiosResponse 42 | } 43 | 44 | export function* saga() { 45 | yield all([]) 46 | } 47 | -------------------------------------------------------------------------------- /src/store/modules/api/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../reducers' 2 | 3 | export const getApi = (state: RootState) => state.api.axios 4 | -------------------------------------------------------------------------------- /src/store/modules/api/types.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance, AxiosResponse } from 'axios' 2 | import { SelectEffect, CallEffect, SagaReturnType } from 'redux-saga/effects' 3 | 4 | export type KeyOf = { 5 | [K in keyof T]: T[K] extends I ? K : never 6 | }[keyof T] 7 | export type KeysOfAxios = KeyOf< 8 | AxiosInstance, 9 | (url: string, ...args: any[]) => Promise> 10 | > 11 | 12 | export type ApiRequestSignatureGenerator = ( 13 | method: K, 14 | ...params: Required> 15 | ) => Generator< 16 | SelectEffect | CallEffect>, 17 | AxiosResponse, 18 | AxiosInstance & AxiosResponse 19 | > 20 | -------------------------------------------------------------------------------- /src/store/modules/auth/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, createAsyncAction } from 'typesafe-actions' 2 | import { UserSettings } from '.' 3 | import { 4 | CreateAdmin, 5 | CreateUser, 6 | DeleteUser, 7 | GetAllUsers, 8 | GetUser, 9 | HasAdmin, 10 | Login, 11 | UpdateUser, 12 | UpdateUserNetworks, 13 | User, 14 | } from './types' 15 | 16 | export const setUser = createAction('setUser')() 17 | 18 | export const setUserSettings = createAction('setUserSettings')() 19 | 20 | export const setAuthError = createAction('setAuthError')() 21 | 22 | export const logout = createAction('logout')() 23 | 24 | export const getAllUsers = createAsyncAction( 25 | 'api_getAllUsers_Request', 26 | 'api_getAllUsers_Success', 27 | 'api_getAllUsers_Failure' 28 | )() 29 | 30 | export const getUser = createAsyncAction( 31 | 'api_getUser_Request', 32 | 'api_getUser_Success', 33 | 'api_getUser_Failure' 34 | )() 35 | 36 | export const login = createAsyncAction( 37 | 'api_login_Request', 38 | 'api_login_Success', 39 | 'api_login_Failure' 40 | )() 41 | 42 | export const hasAdmin = createAsyncAction( 43 | 'api_hasAdmin_Request', 44 | 'api_hasAdmin_Success', 45 | 'api_hasAdmin_Failure' 46 | )() 47 | 48 | export const createAdmin = createAsyncAction( 49 | 'api_createAdmin_Request', 50 | 'api_createAdmin_Success', 51 | 'api_createAdmin_Failure' 52 | )() 53 | 54 | export const createUser = createAsyncAction( 55 | 'api_createUser_Request', 56 | 'api_createUser_Success', 57 | 'api_createUser_Failure' 58 | )() 59 | 60 | export const deleteUser = createAsyncAction( 61 | 'api_deleteUser_Request', 62 | 'api_deleteUser_Success', 63 | 'api_deleteUser_Failure' 64 | )() 65 | 66 | export const updateUser = createAsyncAction( 67 | 'api_updateUser_Request', 68 | 'api_updateUser_Success', 69 | 'api_updateUser_Failure' 70 | )() 71 | 72 | export const updateUserNetworks = createAsyncAction( 73 | 'api_updateUserNetworks_Request', 74 | 'api_updateUserNetworks_Success', 75 | 'api_updateUserNetworks_Failure' 76 | )() 77 | -------------------------------------------------------------------------------- /src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/auth/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getAuth = (state: RootState) => state.auth 5 | 6 | export const getToken = createSelector(getAuth, (auth) => auth.token) 7 | export const getUser = createSelector(getAuth, (auth) => auth.user) 8 | export const getLoggedIn = createSelector( 9 | getAuth, 10 | (auth) => !!auth.user && Date.now() / 1000 < auth.user.exp 11 | ) 12 | export const isLogginIn = createSelector(getAuth, (auth) => auth.isLoggingIn) 13 | export const hasAdmin = createSelector(getAuth, (auth) => auth.hasAdmin) 14 | export const isCreating = createSelector(getAuth, (auth) => auth.isCreating) 15 | export const getUsers = createSelector(getAuth, (auth) => auth.users) 16 | export const hasNetworkError = createSelector( 17 | getAuth, 18 | (auth) => auth.networkError 19 | ) 20 | export const getAuthError = createSelector(getAuth, (auth) => auth.authError) 21 | export const getUserSettings = createSelector( 22 | getAuth, 23 | (auth) => auth.userSettings 24 | ) 25 | export const isInDarkMode = createSelector( 26 | getAuth, 27 | (auth) => !!!auth.userSettings.mode || auth.userSettings.mode === 'dark' 28 | ) 29 | -------------------------------------------------------------------------------- /src/store/modules/auth/types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | name: string 3 | isAdmin: boolean 4 | exp: number 5 | networks: null | Array 6 | groups: null | Array 7 | } 8 | 9 | export interface LocalStorageUserKeyValue { 10 | token: string 11 | user: User 12 | } 13 | 14 | export interface UserSettings { 15 | rowsPerPage: number 16 | username: string 17 | mode: 'dark' | 'light' | undefined 18 | } 19 | 20 | export interface LocalSettings { 21 | userSettings: UserSettings[] 22 | } 23 | 24 | export interface GetAllUsers { 25 | Request: void 26 | Response: Array 27 | } 28 | 29 | export interface GetUser { 30 | Request: void 31 | Response: User 32 | } 33 | 34 | export interface Login { 35 | Request: { 36 | username: string 37 | password: string 38 | } 39 | Response: { 40 | token: string 41 | } 42 | } 43 | 44 | export interface HasAdmin { 45 | Request: void 46 | Response: boolean 47 | } 48 | 49 | export interface CreateAdmin { 50 | Request: { 51 | username: string 52 | password: string 53 | } 54 | Response: {} 55 | } 56 | 57 | export interface CreateUser { 58 | Request: { 59 | username: string 60 | password: string 61 | isadmin: boolean 62 | networks: Array 63 | } 64 | Response: Omit 65 | } 66 | 67 | export interface DeleteUser { 68 | Request: { 69 | username: string 70 | } 71 | Response: { 72 | username: string 73 | } 74 | } 75 | 76 | export interface UpdateUser { 77 | Request: { 78 | username: string 79 | password: string 80 | } 81 | Response: User 82 | } 83 | 84 | export interface UpdateUserNetworks { 85 | Request: { 86 | username: string 87 | isadmin: boolean 88 | networks: Array 89 | groups: Array 90 | } 91 | Response: UpdateUserNetworks['Request'] 92 | } 93 | -------------------------------------------------------------------------------- /src/store/modules/enrollmentkeys/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, createAsyncAction } from 'typesafe-actions' 2 | import { 3 | GetEnrollmentKeysPayload, 4 | DeleteEnrollmentKeyPayload, 5 | CreateEnrollmentKeyPayload, 6 | } from '.' 7 | 8 | export const getEnrollmentKeys = createAsyncAction( 9 | 'EnrollmentKeys_getEnrollmentKeys_Request', 10 | 'EnrollmentKeys_getEnrollmentKeys_Success', 11 | 'EnrollmentKeys_getEnrollmentKeys_Failure' 12 | )() 13 | 14 | export const deleteEnrollmentKey = createAsyncAction( 15 | 'EnrollmentKeys_deleteEnrollmentKey_Request', 16 | 'EnrollmentKeys_deleteEnrollmentKey_Success', 17 | 'EnrollmentKeys_deleteEnrollmentKey_Failure' 18 | )() 19 | 20 | export const createEnrollmentKey = createAsyncAction( 21 | 'EnrollmentKeys_createEnrollmentKey_Request', 22 | 'EnrollmentKeys_createEnrollmentKey_Success', 23 | 'EnrollmentKeys_createEnrollmentKey_Failure' 24 | )() 25 | 26 | export const clearEnrollmentKeys = createAction('EnrollmentKeys_clearEnrollmentKeys')() 27 | -------------------------------------------------------------------------------- /src/store/modules/enrollmentkeys/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/enrollmentkeys/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getEnrollmentKeysState = (state: RootState) => state.enrollmentKeys 5 | 6 | export const isProcessingEnrollmentKeys = createSelector( 7 | getEnrollmentKeysState, 8 | (hostsState) => hostsState.isProcessing 9 | ) 10 | 11 | export const getEnrollmentKeys = createSelector( 12 | getEnrollmentKeysState, 13 | (hostsState) => hostsState.enrollmentKeys 14 | ) 15 | -------------------------------------------------------------------------------- /src/store/modules/enrollmentkeys/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * EnrollmentKey is a key used to control netclient access to netmaker sever. 3 | */ 4 | export interface EnrollmentKey { 5 | value: string // ID 6 | tags: string[] // names 7 | token: string 8 | networks: string[] 9 | expiration: number 10 | uses_remaining: number 11 | unlimited: boolean 12 | } 13 | 14 | export interface GetEnrollmentKeysPayload { 15 | Request: void 16 | Response: EnrollmentKey[] 17 | } 18 | 19 | export interface DeleteEnrollmentKeyPayload { 20 | Request: { 21 | id: EnrollmentKey['value'] 22 | } 23 | Response: { id: EnrollmentKey['value'] } 24 | } 25 | 26 | export interface CreateEnrollmentKeyPayload { 27 | Request: Omit 28 | Response: EnrollmentKey 29 | } 30 | -------------------------------------------------------------------------------- /src/store/modules/hosts/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, createAsyncAction } from 'typesafe-actions' 2 | import { 3 | GetHostsPayload, 4 | UpdateHostPayload, 5 | DeleteHostPayload, 6 | UpdateHostNetworksPayload, 7 | DeleteHostRelayPayload, 8 | CreateHostRelayPayload, 9 | RefreshHostKeysPayload, 10 | } from '.' 11 | 12 | export const getHosts = createAsyncAction( 13 | 'Hosts_getHosts_Request', 14 | 'Hosts_getHosts_Success', 15 | 'Hosts_getHosts_Failure' 16 | )() 17 | 18 | export const updateHost = createAsyncAction( 19 | 'Hosts_updateHosts_Request', 20 | 'Hosts_updateHosts_Success', 21 | 'Hosts_updateHosts_Failure' 22 | )() 23 | 24 | export const updateHostNetworks = createAsyncAction( 25 | 'Hosts_updateHostNetworks_Request', 26 | 'Hosts_updateHostNetworks_Success', 27 | 'Hosts_updateHostNetworks_Failure' 28 | )< 29 | UpdateHostNetworksPayload['Request'], 30 | UpdateHostNetworksPayload['Response'], 31 | Error 32 | >() 33 | 34 | export const refreshHostKeys = createAsyncAction( 35 | 'Hosts_refreshHostKeys_Request', 36 | 'Hosts_refreshHostKeys_Success', 37 | 'Hosts_refreshHostKeys_Failure' 38 | )< 39 | RefreshHostKeysPayload['Request'], 40 | RefreshHostKeysPayload['Response'], 41 | Error 42 | >() 43 | 44 | export const deleteHost = createAsyncAction( 45 | 'Hosts_deleteHosts_Request', 46 | 'Hosts_deleteHosts_Success', 47 | 'Hosts_deleteHosts_Failure' 48 | )() 49 | 50 | export const createHostRelay = createAsyncAction( 51 | 'Hosts_createHostRelay_Request', 52 | 'Hosts_createHostRelay_Success', 53 | 'Hosts_createHostRelay_Failure' 54 | )< 55 | CreateHostRelayPayload['Request'], 56 | CreateHostRelayPayload['Response'], 57 | Error 58 | >() 59 | 60 | export const deleteHostRelay = createAsyncAction( 61 | 'Hosts_deleteHostRelay_Request', 62 | 'Hosts_deleteHostRelay_Success', 63 | 'Hosts_deleteHostRelay_Failure' 64 | )< 65 | DeleteHostRelayPayload['Request'], 66 | DeleteHostRelayPayload['Response'], 67 | Error 68 | >() 69 | 70 | export const clearHosts = createAction('Hosts_clearHosts')() 71 | -------------------------------------------------------------------------------- /src/store/modules/hosts/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/hosts/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getHostsState = (state: RootState) => state.hosts 5 | 6 | export const isProcessingHosts = createSelector( 7 | getHostsState, 8 | (hostsState) => hostsState.isProcessing 9 | ) 10 | 11 | export const getHosts = createSelector( 12 | getHostsState, 13 | (hostsState) => hostsState.hosts 14 | ) 15 | 16 | export const getHostsMap = createSelector( 17 | getHostsState, 18 | (hostsState) => hostsState.hostsMap 19 | ) 20 | -------------------------------------------------------------------------------- /src/store/modules/hosts/types.ts: -------------------------------------------------------------------------------- 1 | import { Interface, Node } from '../node/types' 2 | 3 | /** 4 | * Host is the configuration for a netclient host machine. 5 | * unlike the Node type, a host is independent of a network 6 | * a host may have different representations on different networks as Nodes 7 | */ 8 | export interface Host { 9 | id: string 10 | verbosity: number 11 | firewallinuse: string 12 | version: string 13 | name: string 14 | os: string 15 | debug: boolean 16 | isstatic: boolean 17 | listenport: number 18 | locallistenport: number 19 | proxy_listen_port: number 20 | mtu: number 21 | interfaces: Interface[] 22 | defaultinterface: string // iface name 23 | endpointip: string 24 | publickey: string 25 | macaddress: string 26 | internetgateway: string 27 | nodes: string[] // node ids 28 | proxy_enabled: boolean 29 | isdefault: boolean 30 | isrelayed: boolean 31 | relayed_by: string // host id 32 | isrelay: boolean 33 | relay_hosts: string[] // host ids 34 | } 35 | 36 | export interface NodeCommonDetails { 37 | name: Host['name'] 38 | version: Host['version'] 39 | endpointip: Host['endpointip'] 40 | publickey: Host['publickey'] 41 | os: Host['os'] 42 | listenport: Host['listenport'] 43 | isstatic: Host['isstatic'] 44 | mtu: Host['mtu'] 45 | interfaces: Host['interfaces'] 46 | isrelay: Host['isrelay'] 47 | relay_hosts: Host['relay_hosts'] 48 | isrelayed: Host['isrelayed'] 49 | macaddress: Host['macaddress'] 50 | } 51 | 52 | export interface GetHostsPayload { 53 | Request: void 54 | Response: Host[] 55 | } 56 | 57 | export interface UpdateHostPayload { 58 | Request: Host 59 | Response: Host 60 | } 61 | 62 | export interface RefreshHostKeysPayload { 63 | Request: { id: Host['id'] } 64 | Response: void 65 | } 66 | 67 | export interface UpdateHostNetworksPayload { 68 | Request: { 69 | id: Host['id'] 70 | network: Node['network'] 71 | action: 'join' | 'leave' 72 | } 73 | Response: void 74 | } 75 | 76 | export interface DeleteHostPayload { 77 | Request: { 78 | hostid: string 79 | } 80 | Response: Host 81 | } 82 | 83 | export interface CreateHostRelayPayload { 84 | Request: { 85 | hostid: Host['id'] 86 | relayed_hosts: Host['id'][] 87 | } 88 | Response: Host 89 | } 90 | 91 | export interface DeleteHostRelayPayload { 92 | Request: { 93 | hostid: Host['id'] 94 | } 95 | Response: Host 96 | } 97 | -------------------------------------------------------------------------------- /src/store/modules/network/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/network/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getNetwork = (state: RootState) => state.network 5 | export const isFetchingNetworks = createSelector( 6 | getNetwork, 7 | (network) => network.isFetching 8 | ) 9 | export const getNetworks = createSelector( 10 | getNetwork, 11 | (network) => network.networks 12 | ) 13 | 14 | export const getDnsEntries = createSelector( 15 | getNetwork, 16 | (network) => network.dnsEntries 17 | ) 18 | 19 | export const getTempKey = createSelector( 20 | getNetwork, 21 | (network) => network.tempkey 22 | ) 23 | -------------------------------------------------------------------------------- /src/store/modules/network/utils.ts: -------------------------------------------------------------------------------- 1 | import { Network, NetworkPayload } from './types' 2 | 3 | export const networkToNetworkPayload = (network: Network): NetworkPayload => { 4 | 5 | if (!!network.prosettings) { 6 | if (typeof network.prosettings.allowedgroups === 'string') { 7 | network.prosettings.allowedgroups = convertStringToArray(network.prosettings.allowedgroups) 8 | } 9 | if (typeof network.prosettings.allowedusers === 'string') { 10 | network.prosettings.allowedusers = convertStringToArray(network.prosettings.allowedusers) 11 | } 12 | } 13 | 14 | return { 15 | ...network, 16 | defaultmtu: Number(network.defaultmtu), 17 | defaultlistenport: Number(network.defaultlistenport), 18 | defaultkeepalive: Number(network.defaultkeepalive), 19 | isipv4: network.isipv4 ? 'yes' : 'no', 20 | isipv6: network.isipv6 ? 'yes' : 'no', 21 | defaultudpholepunch: network.defaultudpholepunch ? 'yes' : 'no', 22 | defaultacl: network.defaultacl ? 'yes' : 'no', 23 | prosettings: !!network.prosettings ? { 24 | defaultaccesslevel: Number(network.prosettings.defaultaccesslevel), 25 | defaultuserclientlimit: Number(network.prosettings.defaultuserclientlimit), 26 | defaultusernodelimit: Number(network.prosettings.defaultusernodelimit), 27 | allowedgroups: network.prosettings.allowedgroups, 28 | allowedusers: network.prosettings.allowedusers, 29 | } : undefined 30 | } 31 | } 32 | export const networkPayloadToNetwork = (network: NetworkPayload): Network => { 33 | return { 34 | ...network, 35 | defaultmtu: Number(network.defaultmtu), 36 | defaultlistenport: Number(network.defaultlistenport), 37 | defaultkeepalive: Number(network.defaultkeepalive), 38 | isipv4: network.isipv4 === 'yes', 39 | isipv6: network.isipv6 === 'yes', 40 | defaultudpholepunch: network.defaultudpholepunch === 'yes', 41 | defaultacl: network.defaultacl === 'yes', 42 | prosettings: !!network.prosettings ? { 43 | defaultaccesslevel: Number(network.prosettings.defaultaccesslevel), 44 | defaultuserclientlimit: Number(network.prosettings.defaultuserclientlimit), 45 | defaultusernodelimit: Number(network.prosettings.defaultusernodelimit), 46 | allowedgroups: network.prosettings.allowedgroups, 47 | allowedusers: network.prosettings.allowedusers, 48 | } : undefined 49 | } 50 | } 51 | 52 | const convertStringToArray = (commaSeparatedData: string) => { 53 | const data = commaSeparatedData.split(',') 54 | for (let i = 0; i < data.length; i++) { 55 | data[i] = data[i].trim() 56 | } 57 | return data 58 | } 59 | -------------------------------------------------------------------------------- /src/store/modules/node/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/node/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getNodeState = (state: RootState) => state.node 5 | 6 | export const isFetchingNodes = createSelector( 7 | getNodeState, 8 | (node) => node.isFetching 9 | ) 10 | export const getNodes = createSelector(getNodeState, (node) => node.nodes) 11 | 12 | export const getExtClients = createSelector( 13 | getNodeState, 14 | (node) => node.externalClients 15 | ) 16 | 17 | export const getCurrentQrCode = createSelector( 18 | getNodeState, 19 | (node) => node.qrData 20 | ) 21 | 22 | export const getShouldSignOut = createSelector( 23 | getNodeState, 24 | (node) => node.shouldSignOut 25 | ) 26 | 27 | export const getNodeSort = createSelector(getNodeState, (node) => node.nodeSort) 28 | 29 | export const getNodesMap = createSelector(getNodeState, (node) => node.nodesMap) 30 | -------------------------------------------------------------------------------- /src/store/modules/node/utils.ts: -------------------------------------------------------------------------------- 1 | export function download(filename: string, text: string) { 2 | var element = document.createElement('a') 3 | element.setAttribute( 4 | 'href', 5 | 'data:text/plain;charset=utf-8,' + encodeURIComponent(text) 6 | ) 7 | element.setAttribute('download', filename) 8 | 9 | element.style.display = 'none' 10 | document.body.appendChild(element) 11 | 12 | element.click() 13 | 14 | document.body.removeChild(element) 15 | } 16 | -------------------------------------------------------------------------------- /src/store/modules/pro/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, createAsyncAction } from 'typesafe-actions' 2 | import { UserGroupsPayload, UserGroupDelorCreate } from '.' 3 | import { 4 | NetworkUsersPayload, 5 | NetworkUserDelPayload, 6 | NetworkUserUpdatePayload, 7 | NetworkUserGetPayload, 8 | ProCreateAccessKeyPayload, 9 | } from '.' 10 | import { TempKey } from '../network' 11 | 12 | export const getUserGroups = createAsyncAction( 13 | 'Pro_getUserGroups', 14 | 'Pro_getUserGroups_Success', 15 | 'Pro_getUserGroups_Failure' 16 | )() 17 | 18 | export const createUserGroup = createAsyncAction( 19 | 'Pro_createUserGroup_Request', 20 | 'Pro_createUserGroup_Success', 21 | 'Pro_createUserGroup_Failure' 22 | )() 23 | 24 | export const deleteUserGroup = createAsyncAction( 25 | 'Pro_deleteUserGroup_Request', 26 | 'Pro_deleteUserGroup_Success', 27 | 'Pro_deleteUserGroup_Failure' 28 | )() 29 | 30 | export const getNetworkUsers = createAsyncAction( 31 | 'Pro_getNetworkUsers_Request', 32 | 'Pro_getNetworkUsers_Success', 33 | 'Pro_getNetworkUsers_Failure' 34 | )() 35 | 36 | export const deleteNetworkUser = createAsyncAction( 37 | 'Pro_deleteNetworkUser_Request', 38 | 'Pro_deleteNetworkUser_Success', 39 | 'Pro_deleteNetworkUser_Failure' 40 | )() 41 | 42 | export const updateNetworkUser = createAsyncAction( 43 | 'Pro_updateNetworkUser_Request', 44 | 'Pro_updateNetworkUser_Success', 45 | 'Pro_updateNetworkUser_Failure' 46 | )< 47 | NetworkUserUpdatePayload['Request'], 48 | NetworkUserUpdatePayload['Response'], 49 | Error 50 | >() 51 | 52 | export const getNetworkUserData = createAsyncAction( 53 | 'Pro_getNetworkUserData_Request', 54 | 'Pro_getNetworkUserData_Success', 55 | 'Pro_getNetworkUserData_Failure' 56 | )< 57 | NetworkUserGetPayload['Request'], 58 | NetworkUserGetPayload['Response'], 59 | Error 60 | >() 61 | 62 | export const proCreateAccessKey = createAsyncAction( 63 | 'Pro_proCreateAccessKey_Request', 64 | 'Pro_proCreateAccessKey_Success', 65 | 'Pro_proCreateAccessKey_Failure' 66 | )< 67 | ProCreateAccessKeyPayload['Request'], 68 | ProCreateAccessKeyPayload['Response'], 69 | Error 70 | >() 71 | 72 | export const clearCurrentAccessKey = createAction('clearCurrentAccessKey')() 73 | export const setCurrentAccessKey = createAction('setCurrentAccessKey')() 74 | -------------------------------------------------------------------------------- /src/store/modules/pro/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/pro/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getProState = (state: RootState) => state.pro 5 | 6 | export const isProcessing = createSelector( 7 | getProState, 8 | (pro) => pro.isProcessing 9 | ) 10 | 11 | export const userGroups = createSelector(getProState, (pro) => pro.userGroups) 12 | 13 | export const networkUsers = createSelector( 14 | getProState, 15 | (pro) => pro.networkUsers 16 | ) 17 | 18 | export const networkUserData = createSelector( 19 | getProState, 20 | (pro) => pro.networkUserData 21 | ) 22 | 23 | export const getNetworkUserNetworks = createSelector(getProState, (pro) => 24 | Object.keys(pro.networkUserData) 25 | ) 26 | 27 | export const getCurrentAccessKey = createSelector(getProState, (pro) => pro.currentAccessKey) 28 | 29 | export const isCreatingGroup = createSelector(getProState, (pro) => pro.isCreatingGroup) 30 | -------------------------------------------------------------------------------- /src/store/modules/pro/types.ts: -------------------------------------------------------------------------------- 1 | import { MutableRequired } from '../acls' 2 | import { Node, ExternalClient, Network, NetworkPayload, AccessKey } from '../../types' 3 | 4 | export type UserGroups = MutableRequired<{ [groupName: string]: void }> 5 | 6 | export interface UserGroupsPayload { 7 | Request: void 8 | Response: UserGroups 9 | } 10 | 11 | export interface UserGroupDelorCreate { 12 | Request: { groupName: string } 13 | Response: { groupName: string } 14 | } 15 | 16 | export interface NetworkUser { 17 | accesslevel: number 18 | clientlimit: number 19 | nodelimit: number 20 | id: string 21 | clients: Array 22 | nodes: Array 23 | } 24 | 25 | export interface NetworkUserData { 26 | nodes: Array 27 | clients: Array 28 | vpns: Array 29 | networks: Array 30 | user: NetworkUser 31 | } 32 | 33 | export interface NetworkUserDataPayload { 34 | nodes: Array 35 | clients: Array 36 | vpns: Array 37 | networks: Array 38 | user: NetworkUser 39 | } 40 | 41 | export type NetworksUsersMap = MutableRequired<{ 42 | [networkName: string]: NetworkUser[] 43 | }> 44 | 45 | export type NetworkUserDataMap = MutableRequired<{ 46 | [networkName: string]: NetworkUserData 47 | }> 48 | 49 | export type NetworkUserDataMapPayload = MutableRequired<{ 50 | [networkName: string]: NetworkUserDataPayload 51 | }> 52 | 53 | export interface NetworkUsersPayload { 54 | Request: void 55 | Response: NetworksUsersMap 56 | } 57 | 58 | export interface NetworkUserDelPayload { 59 | Request: { networkName: string; networkUserID: string } 60 | Response: { networkName: string; networkUserID: string } 61 | } 62 | 63 | export interface NetworkUserUpdatePayload { 64 | Request: { networkName: string; networkUser: NetworkUser } 65 | Response: { networkName: string; networkUser: NetworkUser } 66 | } 67 | 68 | export interface NetworkUserGetPayload { 69 | Request: { networkUserID: string } 70 | Response: NetworkUserDataMapPayload 71 | } 72 | 73 | export interface ProCreateAccessKeyPayload { 74 | Request: { 75 | netid: string 76 | newAccessKey: { 77 | name: string 78 | uses: number 79 | } 80 | } 81 | Response: { 82 | netid: string 83 | newAccessKey: AccessKey 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/store/modules/pro/utils.ts: -------------------------------------------------------------------------------- 1 | import { networkPayloadToNetwork } from "../network/utils"; 2 | import { NetworkUserData, NetworkUserDataMap, NetworkUserDataMapPayload } from "./types"; 3 | 4 | export function netUserDataPayloadToNetUserData(payload: NetworkUserDataMapPayload): NetworkUserDataMap { 5 | let newMap = {} as NetworkUserDataMap 6 | 7 | if (!!payload) { 8 | Object.keys(payload).map(network => { 9 | newMap[network] = {} as NetworkUserData 10 | if (payload[network]) { 11 | if (payload[network].nodes) 12 | newMap[network].nodes = payload[network].nodes 13 | if (payload[network].vpns) 14 | newMap[network].vpns = payload[network].vpns 15 | if (payload[network].networks) 16 | newMap[network].networks = payload[network].networks.map(n => networkPayloadToNetwork(n)) 17 | if (payload[network].clients) 18 | newMap[network].clients = payload[network].clients 19 | if (payload[network].user) 20 | newMap[network].user = payload[network].user 21 | } 22 | return null 23 | }) 24 | } 25 | 26 | return newMap 27 | } 28 | -------------------------------------------------------------------------------- /src/store/modules/router/Component.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react' 2 | import { useDispatch } from 'react-redux' 3 | import { useHistory } from 'react-router-dom' 4 | import { LocationListener } from 'history' 5 | import { routerActions } from './actions' 6 | 7 | export const RouterState = () => { 8 | const history = useHistory() 9 | const dispatch = useDispatch() 10 | 11 | const historyListener = useCallback( 12 | (location, action) => { 13 | dispatch( 14 | routerActions[action]({ 15 | location, 16 | history, 17 | }) 18 | ) 19 | }, 20 | [dispatch, history] 21 | ) 22 | 23 | useEffect(() => { 24 | historyListener(history.location, 'POP') 25 | return history.listen(historyListener) 26 | }, [history, historyListener]) 27 | 28 | return null 29 | } 30 | -------------------------------------------------------------------------------- /src/store/modules/router/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from 'typesafe-actions' 2 | import { RouterAction } from './types' 3 | 4 | const RouterActionPrefix = `router` 5 | 6 | export const routerPush = createAction( 7 | `${RouterActionPrefix}/PUSH` 8 | )() 9 | 10 | export const routerPop = createAction( 11 | `${RouterActionPrefix}/POP` 12 | )() 13 | 14 | export const routerReplace = createAction( 15 | `${RouterActionPrefix}/REPLACE` 16 | )() 17 | 18 | export const routerActions = { 19 | POP: routerPop, 20 | PUSH: routerPush, 21 | REPLACE: routerReplace, 22 | } 23 | -------------------------------------------------------------------------------- /src/store/modules/router/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/router/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'typesafe-actions' 2 | import { routerPop, routerPush, routerReplace } from './actions' 3 | import { RouterState } from './types' 4 | 5 | export const reducer = createReducer({}).handleAction( 6 | [routerPush, routerReplace, routerPop], 7 | (state, action) => ({ 8 | prevLocation: state.location, 9 | history: action.payload.history, 10 | location: action.payload.location, 11 | }) 12 | ) 13 | -------------------------------------------------------------------------------- /src/store/modules/router/saga.ts: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects' 2 | 3 | export function* saga() { 4 | yield all([]) 5 | } 6 | -------------------------------------------------------------------------------- /src/store/modules/router/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getRouter = (state: RootState) => state.router 5 | 6 | export const getLocation = createSelector(getRouter, (state) => state.location) 7 | 8 | export const getHistory = createSelector(getRouter, (state) => state.history) 9 | -------------------------------------------------------------------------------- /src/store/modules/router/types.ts: -------------------------------------------------------------------------------- 1 | import { History, Location } from 'history' 2 | 3 | export interface RouterState { 4 | history?: History 5 | location?: Location 6 | prevLocation?: Location 7 | } 8 | 9 | export interface RouterAction { 10 | history: History 11 | location: Location 12 | } 13 | -------------------------------------------------------------------------------- /src/store/modules/server/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, createAsyncAction } from 'typesafe-actions' 2 | import { 3 | GetServerConfigPayload, 4 | LogsPayload, 5 | Metrics, 6 | ExtMetricsPayload, 7 | AllNodesMetricsPayload, 8 | NetworkMetricsPayload, 9 | } from './types' 10 | 11 | export const getServerConfig = createAsyncAction( 12 | 'api_getServerConfig_Request', 13 | 'api_getServerConfig_Success', 14 | 'api_getServerConfig_Failure' 15 | )< 16 | GetServerConfigPayload['Request'], 17 | GetServerConfigPayload['Response'], 18 | Error 19 | >() 20 | 21 | export const getServerLogs = createAsyncAction( 22 | 'api_getServerLogs_Request', 23 | 'api_getServerLogs_Success', 24 | 'api_getServerLogs_Failure' 25 | )() 26 | 27 | export const getNodeMetrics = createAsyncAction( 28 | 'api_getNodeMetrics_Request', 29 | 'api_getNodeMetrics_Success', 30 | 'api_getNodeMetrics_Failure' 31 | )< 32 | AllNodesMetricsPayload['Request'], 33 | AllNodesMetricsPayload['Response'], 34 | Error 35 | >() 36 | 37 | export const getMetrics = createAsyncAction( 38 | 'api_getMetrics_Request', 39 | 'api_getMetrics_Success', 40 | 'api_getMetrics_Failure' 41 | )() 42 | 43 | export const getNetworkMetrics = createAsyncAction( 44 | 'api_getNetworkMetrics_Request', 45 | 'api_getNetworkMetrics_Success', 46 | 'api_getNetworkMetrics_Failure' 47 | )() 48 | 49 | export const getExtMetrics = createAsyncAction( 50 | 'api_getExtMetrics_Request', 51 | 'api_getExtMetrics_Success', 52 | 'api_getExtMetrics_Failure' 53 | )() 54 | 55 | export const clearCurrentMetrics = createAction('clearCurrentMetrics')() 56 | -------------------------------------------------------------------------------- /src/store/modules/server/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { reducer } from './reducer' 3 | export { saga } from './saga' 4 | export * as selectors from './selectors' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /src/store/modules/server/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { RootState } from '../../reducers' 3 | 4 | const getServer = (state: RootState) => state.server 5 | export const getServerConfig = createSelector( 6 | getServer, 7 | (server) => server.config 8 | ) 9 | export const isFetchingServerConfig = createSelector( 10 | getServer, 11 | (server) => server.isFetching 12 | ) 13 | export const getServerLogs = createSelector(getServer, (server) => server.logs) 14 | export const getNodeMetrics = createSelector( 15 | getServer, 16 | (server) => server.nodeMetrics 17 | ) 18 | export const getAttempts = createSelector( 19 | getServer, 20 | (server) => server.attempts 21 | ) 22 | export const hasFetchedNodeMetrics = createSelector( 23 | getServer, 24 | (server) => server.fetchedNodeMetrics 25 | ) 26 | 27 | export const getNetworkExtMetrics = (network: string) => 28 | createSelector(getServer, (server) => server.extMetrics[network]) 29 | 30 | export const getNodeMetric = (nodeid: string) => 31 | createSelector(getNodeMetrics, (nodeMetrics) => nodeMetrics[nodeid]) 32 | 33 | export const getNetworkMetrics = (network: string) => 34 | createSelector(getServer, (server) => server.networkMetrics[network]) 35 | 36 | export const isFetchingClientMetrics = createSelector( 37 | getServer, 38 | (server) => server.fetchingExtMetrics 39 | ) 40 | -------------------------------------------------------------------------------- /src/store/modules/toast/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from 'typesafe-actions' 2 | import { ToastPayload } from './types' 3 | 4 | export const toast = createAction('toast_toast')() 5 | -------------------------------------------------------------------------------- /src/store/modules/toast/index.ts: -------------------------------------------------------------------------------- 1 | export * as actions from './actions' 2 | export { saga } from './saga' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /src/store/modules/toast/types.ts: -------------------------------------------------------------------------------- 1 | import { ToastContent, ToastOptions, UpdateOptions } from 'react-toastify' 2 | import { PayloadActionCreator } from 'typesafe-actions' 3 | 4 | export type ToastPromiseType = string | UpdateOptions 5 | export type ToastPromiseFunctionType = (value: T) => ToastPromiseType 6 | export interface ToastPromiseParams { 7 | pending: ToastPromiseType 8 | success: ToastPromiseType | ToastPromiseFunctionType 9 | error: ToastPromiseType | ToastPromiseFunctionType 10 | } 11 | 12 | export interface ToastPayload { 13 | method: 'info' | 'warn' | 'success' | 'error' 14 | content: ToastContent 15 | options?: ToastOptions 16 | } 17 | 18 | export interface AsyncToastPayload { 19 | params: ToastPromiseParams 20 | promise: { 21 | method: (...args: any[]) => Promise 22 | params: any[] 23 | } 24 | options?: ToastOptions 25 | } 26 | 27 | export interface GeneratorToastPayload { 28 | params: ToastPromiseParams 29 | success: PayloadActionCreator 30 | error: PayloadActionCreator 31 | options?: ToastOptions 32 | } 33 | -------------------------------------------------------------------------------- /src/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { StateType } from 'typesafe-actions' 3 | 4 | import { reducer as auth } from './modules/auth' 5 | import { reducer as api } from './modules/api' 6 | import { reducer as network } from './modules/network' 7 | import { reducer as node } from './modules/node' 8 | import { reducer as server } from './modules/server' 9 | import { reducer as router } from './modules/router' 10 | import { reducer as acls } from './modules/acls' 11 | import { reducer as pro } from './modules/pro' 12 | import { reducer as hosts } from './modules/hosts' 13 | import { reducer as enrollmentKeys } from './modules/enrollmentkeys' 14 | 15 | export const createRootReducer = () => 16 | combineReducers({ 17 | auth, 18 | api, 19 | network, 20 | node, 21 | server, 22 | router, 23 | acls, 24 | pro, 25 | hosts, 26 | enrollmentKeys, 27 | }) 28 | 29 | export type RootState = StateType> 30 | -------------------------------------------------------------------------------- /src/store/sagas.ts: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects' 2 | 3 | import { saga as auth } from './modules/auth' 4 | import { saga as api } from './modules/api' 5 | import { saga as network } from './modules/network' 6 | import { saga as node } from './modules/node' 7 | import { saga as server } from './modules/server' 8 | import { saga as toast } from './modules/toast' 9 | import { saga as router } from './modules/router' 10 | import { saga as acls } from './modules/acls' 11 | import { saga as pro } from './modules/pro' 12 | import { saga as hosts } from './modules/hosts' 13 | import { saga as enrollmentKeys } from './modules/enrollmentkeys' 14 | 15 | export const createRootSaga = () => { 16 | function* rootSaga() { 17 | yield all([ 18 | auth(), 19 | api(), 20 | network(), 21 | node(), 22 | server(), 23 | toast(), 24 | router(), 25 | acls(), 26 | pro(), 27 | hosts(), 28 | enrollmentKeys(), 29 | ]) 30 | } 31 | return rootSaga 32 | } 33 | -------------------------------------------------------------------------------- /src/store/selectors.ts: -------------------------------------------------------------------------------- 1 | export { selectors as authSelectors } from './modules/auth' 2 | export { selectors as apiSelectors } from './modules/api' 3 | export { selectors as networkSelectors } from './modules/network' 4 | export { selectors as nodeSelectors } from './modules/node' 5 | export { selectors as serverSelectors } from './modules/server' 6 | export { selectors as toastSelectors } from './modules/server' 7 | export { selectors as routerSelectors } from './modules/router' 8 | export { selectors as aclSelectors } from './modules/acls' 9 | export { selectors as proSelectors } from './modules/pro' 10 | export { selectors as hostsSelectors } from './modules/hosts' 11 | export { selectors as enrollmentKeysSelectors } from './modules/enrollmentkeys' 12 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | export type { RootState } from './reducers' 2 | export * from './selectors' 3 | export * from './actions' 4 | 5 | export * from './modules/auth/types' 6 | export * from './modules/network/types' 7 | export * from './modules/node/types' 8 | export * from './modules/server/types' 9 | export * from './modules/api/types' 10 | export * from './modules/toast/types' 11 | export * from './modules/router/types' 12 | export * from './modules/acls/types' 13 | export * from './modules/pro/types' 14 | export * from './modules/hosts/types' 15 | export * from './modules/enrollmentkeys/types' 16 | -------------------------------------------------------------------------------- /src/types/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare type DeepPartial = { 4 | [P in keyof T]?: T[P] extends Array 5 | ? Array> 6 | : T[P] extends ReadonlyArray 7 | ? ReadonlyArray> 8 | : DeepPartial 9 | } 10 | 11 | export declare type Modify = Omit & R 12 | 13 | export { Theme, ThemeOptions } from '@mui/material/styles' 14 | declare module '@mui/material/styles' { 15 | interface Theme { 16 | status: { 17 | danger: string 18 | } 19 | } 20 | // allow configuration using `createTheme` 21 | interface ThemeOptions extends DeepPartial {} 22 | } 23 | 24 | declare module '@mui/styles' { 25 | import { Theme, ThemeOptions } from '@mui/material/styles' 26 | interface DefaultTheme extends Theme {} 27 | } 28 | 29 | declare module 'react' { 30 | function forwardRef( 31 | render: (props: P, ref: React.Ref) => React.ReactElement | null 32 | ): (props: P & React.RefAttributes) => React.ReactElement | null 33 | } 34 | -------------------------------------------------------------------------------- /src/util/enrollmentkeys.ts: -------------------------------------------------------------------------------- 1 | import { EnrollmentKey } from '~store/modules/enrollmentkeys' 2 | 3 | export function isEnrollmentKeyValid(key: EnrollmentKey): boolean { 4 | if (key === undefined || key === null) { 5 | return false 6 | } 7 | if (key.uses_remaining > 0) { 8 | return true 9 | } 10 | if (new Date(key.expiration).getTime() > Date.now()) { 11 | return true 12 | } 13 | 14 | return key.unlimited 15 | } 16 | -------------------------------------------------------------------------------- /src/util/errorpage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Grid } from '@mui/material' 2 | import Typography from '@mui/material/Typography' 3 | import { useTranslation } from 'react-i18next' 4 | import Logo from '../netmaker-logo.png' 5 | import DarkLogo from '../netmaker-logo-2.png' 6 | import { useSelector } from 'react-redux' 7 | import { authSelectors } from '~store/selectors' 8 | import WarningIcon from '@mui/icons-material/Warning' 9 | import { Link } from 'react-router-dom' 10 | 11 | export const NotFound: React.FC = () => { 12 | const { t } = useTranslation() 13 | const inDarkMode = useSelector(authSelectors.isInDarkMode) 14 | 15 | const styles = { 16 | logo: { 17 | objectFit: 'cover', 18 | // height: 70, 19 | maxWidth: '40%', 20 | height: 'auto', 21 | width: 'auto', 22 | minWidth: '8em', 23 | } as React.DetailedHTMLProps< 24 | React.ImgHTMLAttributes, 25 | HTMLImageElement 26 | >, 27 | center: { 28 | textAlign: 'center' 29 | } as React.DetailedHTMLProps< 30 | React.ImgHTMLAttributes, 31 | HTMLImageElement 32 | >, 33 | } 34 | 35 | return ( 36 |
37 | 38 | 39 | 40 | 41 | 42 |
50 | {`${t('error.notfound')}`} 51 |
52 |
53 | 54 | 61 | 62 |
63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/util/genericerror.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@mui/material' 2 | import Typography from '@mui/material/Typography' 3 | import { useTranslation } from 'react-i18next' 4 | import Logo from '../netmaker-logo.png' 5 | import DarkLogo from '../netmaker-logo-2.png' 6 | import { useSelector } from 'react-redux' 7 | import { authSelectors } from '~store/selectors' 8 | import WarningIcon from '@mui/icons-material/Report' 9 | 10 | export const GenericError: React.FC<{message?: string}> = ({message}) => { 11 | const { t } = useTranslation() 12 | const inDarkMode = useSelector(authSelectors.isInDarkMode) 13 | 14 | const styles = { 15 | logo: { 16 | objectFit: 'cover', 17 | // height: 70, 18 | maxWidth: '40%', 19 | height: 'auto', 20 | width: 'auto', 21 | minWidth: '8em', 22 | } as React.DetailedHTMLProps< 23 | React.ImgHTMLAttributes, 24 | HTMLImageElement 25 | >, 26 | center: { 27 | textAlign: 'center' 28 | } as React.DetailedHTMLProps< 29 | React.ImgHTMLAttributes, 30 | HTMLImageElement 31 | >, 32 | } 33 | 34 | return ( 35 |
36 | 37 | 38 | 39 | 40 | 41 |
49 | {`${message || t('error.noresults')}`} 50 |
51 |
52 | 53 | Netmaker makes networks. 58 | 59 |
60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/util/hosts.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { createSelector } from 'reselect' 4 | import { RootState } from 'typesafe-actions' 5 | import { Host } from '~store/modules/hosts' 6 | import { hostsSelectors } from '~store/selectors' 7 | 8 | const hostByIdPredicate = (id: Host['id']) => (host: Host) => host.id === id 9 | 10 | const makeSelectHostByID = () => 11 | createSelector( 12 | hostsSelectors.getHosts, 13 | (_: RootState, id: Host['id']) => id, 14 | (hosts, id) => hosts.find(hostByIdPredicate(id)) 15 | ) 16 | 17 | export const useGetHostById = (id: Host['id']) => { 18 | const selectHost = useMemo(makeSelectHostByID, []) 19 | return useSelector((state) => 20 | selectHost(state, id) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/util/network.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { createSelector } from 'reselect' 4 | import { RootState } from 'typesafe-actions' 5 | import { Network } from '~modules/network/types' 6 | import { Node } from '~modules/node/types' 7 | import { networkSelectors, nodeSelectors } from '~store/selectors' 8 | 9 | const networkByNetIdPredicate = (netid: string) => (network: Network) => 10 | network.netid === netid 11 | 12 | const makeSelectNetwork = () => 13 | createSelector( 14 | networkSelectors.getNetworks, 15 | (_: RootState, netid: string) => netid, 16 | (networks, netid) => networks.find(networkByNetIdPredicate(netid)) 17 | ) 18 | 19 | export const useNetwork = (netid: string) => { 20 | const selectNetwork = useMemo(makeSelectNetwork, []) 21 | return useSelector((state) => 22 | selectNetwork(state, netid) 23 | ) 24 | } 25 | 26 | const nodesByNetworkIdPredicate = (id: Network['netid']) => (node: Node) => 27 | node.network === id 28 | 29 | const makeSelectNodeByNetworkId = () => 30 | createSelector( 31 | nodeSelectors.getNodes, 32 | (_: RootState, id: Network['netid']) => id, 33 | (nodes, id) => nodes.filter(nodesByNetworkIdPredicate(id)) 34 | ) 35 | 36 | export const useNodesByNetworkId = (id: Network['netid']) : Array | undefined => { 37 | const selectNodes = useMemo(makeSelectNodeByNetworkId, []) 38 | return useSelector | undefined>((state) => 39 | selectNodes(state, id) 40 | ) 41 | } 42 | 43 | export const useNodeCountByNetworkId = (id: Network['netid']) => { 44 | const nodes = useNodesByNetworkId(id) 45 | if (nodes) { 46 | return nodes.length 47 | } 48 | return 0 49 | } 50 | 51 | // Convert byte array to string. 52 | // Useful for converting IP addresses from backend to readable string 53 | export function byteArrayToString(byteArray: any): string { 54 | return btoa(String.fromCharCode(...new Uint8Array(byteArray))); 55 | } 56 | -------------------------------------------------------------------------------- /src/util/query.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom' 2 | 3 | export function useQuery() { 4 | return new URLSearchParams(useLocation().search) 5 | } 6 | -------------------------------------------------------------------------------- /src/util/regex.ts: -------------------------------------------------------------------------------- 1 | export const correctUserNameRegex = new RegExp( 2 | /^([a-zA-Z0-9,\-,.]{3,40})|([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4})$/i 3 | ) 4 | 5 | export const correctPasswordRegex = new RegExp(/^.{5,64}$/i) 6 | 7 | // regex for ipv6 with cidr 8 | export const correctIpv6CidrRegex = new RegExp( 9 | /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/i 10 | ) 11 | 12 | export const correctIPv4CidrRegex = new RegExp( 13 | /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/\d{1,2}$/i 14 | ) 15 | 16 | export const ipv4AddressRegex = new RegExp( 17 | /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/i 18 | ) 19 | -------------------------------------------------------------------------------- /src/util/unixTime.ts: -------------------------------------------------------------------------------- 1 | export const timeConverter = (UNIX_timestamp: number) => { 2 | const a = new Date(UNIX_timestamp * 1000) 3 | const months = [ 4 | 'Jan', 5 | 'Feb', 6 | 'Mar', 7 | 'Apr', 8 | 'May', 9 | 'Jun', 10 | 'Jul', 11 | 'Aug', 12 | 'Sep', 13 | 'Oct', 14 | 'Nov', 15 | 'Dec', 16 | ] 17 | const year = a.getFullYear() 18 | const month = months[a.getMonth()] 19 | const date = a.getDate() 20 | const hour = a.getHours() 21 | const min = a.getMinutes() 22 | const sec = a.getSeconds() 23 | const time = 24 | date + ' ' + month + ', ' + year + ' - ' + hour + ':' + min + ':' + sec 25 | return time 26 | } 27 | export const datePickerConverter = (UNIX_timestamp: number) => { 28 | const a = new Date(UNIX_timestamp * 1000) 29 | const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 30 | const year = a.getFullYear() 31 | const month = 32 | months[a.getMonth()] < 10 33 | ? '0' + months[a.getMonth()] 34 | : months[a.getMonth()] 35 | const date = a.getDate() < 10 ? '0' + a.getDate() : a.getDate() 36 | const hour = a.getHours() < 10 ? '0' + a.getHours() : a.getHours() 37 | const min = a.getMinutes() < 10 ? '0' + a.getMinutes() : a.getMinutes() 38 | const time = year + '-' + month + '-' + date + 'T' + hour + ':' + min 39 | return time 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | "exclude": [ 28 | "src/Components" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~util/*": ["./src/util/*"], 6 | "~components/*": ["./src/components/*"], 7 | "~store/*": ["./src/store/*"], 8 | "~modules/*": ["./src/store/modules/*"] 9 | } 10 | } 11 | } --------------------------------------------------------------------------------