Web UI improvements
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/circuitbreaker
|
||||
/webui-build
|
||||
**/node_modules
|
||||
**/node_modules
|
||||
|
|
|
|||
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"prettier.configPath": "web/.prettierrc"
|
||||
}
|
||||
21
Dockerfile
|
|
@ -1,15 +1,12 @@
|
|||
### Build frontend
|
||||
FROM node:15.4 as build_frontend
|
||||
|
||||
WORKDIR /react-app
|
||||
|
||||
COPY webui/package*.json .
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY webui .
|
||||
|
||||
RUN npm run build
|
||||
# Install dependencies only when needed
|
||||
FROM node:lts-alpine AS build_frontend
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
COPY web .
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn build
|
||||
RUN yarn export
|
||||
|
||||
### Build backend
|
||||
FROM golang:1.19-alpine AS build_backend
|
||||
|
|
|
|||
13
stub.go
|
|
@ -21,6 +21,19 @@ var stubNodes = []string{
|
|||
"NordicRails",
|
||||
"Stone Of Jordan",
|
||||
"Mushi",
|
||||
"Boltz",
|
||||
"CryptoChill",
|
||||
"ACINQ",
|
||||
"Lightning Labs",
|
||||
"Bottlepay",
|
||||
"NYDIG",
|
||||
"Bitrefill",
|
||||
"Lightning.Watch",
|
||||
"OpenNode",
|
||||
"LND2",
|
||||
"coincharge",
|
||||
"LightningJoule",
|
||||
"Mafia",
|
||||
}
|
||||
|
||||
type stubChannel struct {
|
||||
|
|
|
|||
1
web/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
.git
|
||||
8
web/.eslintignore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
jest.config.js
|
||||
next.config.js
|
||||
server.js
|
||||
babel-jest.js
|
||||
next-i18next.config.js
|
||||
|
||||
/public
|
||||
/coverage
|
||||
41
web/.eslintrc.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: ['airbnb', 'airbnb/hooks', 'airbnb-typescript', 'prettier'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
root: true,
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: [
|
||||
'**/*.test.tsx',
|
||||
'**/*.test.ts',
|
||||
'src/testing/*',
|
||||
'jest-setup.ts',
|
||||
],
|
||||
},
|
||||
],
|
||||
'import/prefer-default-export': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'jsx-a11y/media-has-caption': 'off',
|
||||
'jsx-a11y/anchor-is-valid': 'off',
|
||||
'no-console': 'off',
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
},
|
||||
};
|
||||
37
webui/.gitignore → web/.gitignore
vendored
|
|
@ -1,5 +1,9 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# sidecar env
|
||||
.env.sidecar
|
||||
.env.cookie
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
|
@ -8,13 +12,40 @@
|
|||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# local
|
||||
.vscode
|
||||
|
||||
# yalc
|
||||
/.yalc
|
||||
yalc.lock
|
||||
|
||||
# typescript
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# mac OS
|
||||
../.DS_Store
|
||||
4
web/.husky/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
cd web && yarn check-lint && yarn check-types
|
||||
7
web/.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"endOfLine": "auto",
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}
|
||||
45
web/Dockerfile
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Install dependencies only when needed
|
||||
FROM node:lts-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:lts-alpine AS builder
|
||||
ARG NPM_TOKEN
|
||||
ENV NPM_TOKEN="${NPM_TOKEN}"
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
RUN yarn build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:lts-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# You only need to copy next.config.js if you are NOT using the default configuration
|
||||
COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/next-i18next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
20
web/i18n.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import Backend from 'i18next-http-backend';
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
load: 'languageOnly',
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
ns: 'common',
|
||||
defaultNS: 'common',
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
5
web/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
15
web/next.config.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
async rewrites() {
|
||||
if (process.env.NODE_ENV === 'development')
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://localhost:9235/api/:path*',
|
||||
},
|
||||
];
|
||||
else return [];
|
||||
},
|
||||
};
|
||||
56
web/package.json
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "checkout",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"export": "next export -o ../webui-build",
|
||||
"test": "yarn jest --runInBand",
|
||||
"test-watch": "yarn jest --watch",
|
||||
"check-types": "tsc --pretty --noEmit",
|
||||
"check-lint": "eslint . --ext ts --ext tsx --ext js",
|
||||
"prepare": "cd .. && husky install web/.husky",
|
||||
"build-export": "yarn build && yarn export"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/base": "^5.0.0-alpha.120",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.11",
|
||||
"@tanstack/react-query": "^4.24.10",
|
||||
"axios": "^0.24.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"i18next": "^22.4.12",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "^13.0.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"notistack": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^17.0.14",
|
||||
"@types/react": "18.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^5.1.0",
|
||||
"@typescript-eslint/parser": "^5.1.0",
|
||||
"eslint": "<8.0.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-typescript": "^14.0.1",
|
||||
"eslint-config-next": "^13.0.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint-plugin-react-hooks": "^1.7.0",
|
||||
"husky": "^8.0.0",
|
||||
"prettier": "^2.4.1",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
BIN
web/public/fonts/HelveticaNeue.eot
Normal file
BIN
web/public/fonts/HelveticaNeue.ttf
Normal file
BIN
web/public/fonts/HelveticaNeue.woff
Normal file
BIN
web/public/fonts/HelveticaNeue.woff2
Normal file
BIN
web/public/fonts/HelveticaNeueBold.eot
Normal file
BIN
web/public/fonts/HelveticaNeueBold.ttf
Normal file
BIN
web/public/fonts/HelveticaNeueBold.woff2
Normal file
3
web/public/icons/caret-down.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 4L6 7.99971L10 4" stroke="#060814" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
3
web/public/icons/check-primary.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 4.5L3.74991 7L9 1" stroke="#2C55FB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
3
web/public/icons/check.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 4.5L3.74991 7L9 1" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 215 B |
3
web/public/icons/close.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6663 4.27301L11.7263 3.33301L7.99967 7.05967L4.27301 3.33301L3.33301 4.27301L7.05967 7.99967L3.33301 11.7263L4.27301 12.6663L7.99967 8.93967L11.7263 12.6663L12.6663 11.7263L8.93967 7.99967L12.6663 4.27301Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 338 B |
3
web/public/icons/columns.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.6 1L6.6 17M11.4 1L11.4 17M4.2 17H13.8C15.5673 17 17 15.5673 17 13.8V4.2C17 2.43269 15.5673 1 13.8 1H4.2C2.43269 1 1 2.43269 1 4.2V13.8C1 15.5673 2.43269 17 4.2 17Z" stroke="#787A8A" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 318 B |
3
web/public/icons/edit-disabled.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.7121 4.07395L11.9181 1.2781L12.8349 0.360714C13.0823 0.113165 13.3915 -0.00696882 13.7626 0.000312037C14.1337 0.0075929 14.4429 0.135008 14.6903 0.382557L15.6289 1.32179C15.8763 1.56934 16 1.87513 16 2.23918C16 2.60322 15.8763 2.90902 15.6289 3.15656L14.7121 4.07395ZM13.7954 4.99134L2.794 16H0V13.2041L11.0014 2.19549L13.7954 4.99134Z" fill="#828CC5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
3
web/public/icons/edit.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.7121 4.07395L11.9181 1.2781L12.8349 0.360714C13.0823 0.113165 13.3915 -0.00696882 13.7626 0.000312037C14.1337 0.0075929 14.4429 0.135008 14.6903 0.382557L15.6289 1.32179C15.8763 1.56934 16 1.87513 16 2.23918C16 2.60322 15.8763 2.90902 15.6289 3.15656L14.7121 4.07395ZM13.7954 4.99134L2.794 16H0V13.2041L11.0014 2.19549L13.7954 4.99134Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 468 B |
4
web/public/icons/exclamation.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0002 17.5C10.9206 17.5 11.6668 16.7538 11.6668 15.8333C11.6668 14.9129 10.9206 14.1667 10.0002 14.1667C9.07969 14.1667 8.3335 14.9129 8.3335 15.8333C8.3335 16.7538 9.07969 17.5 10.0002 17.5Z" fill="white"/>
|
||||
<path d="M8.3335 2.5H11.6668V12.5H8.3335V2.5Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
3
web/public/icons/info.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 0C5.61553 0 4.26215 0.410543 3.11101 1.17971C1.95987 1.94888 1.06266 3.04213 0.532846 4.32122C0.003033 5.6003 -0.13559 7.00776 0.134506 8.36563C0.404603 9.7235 1.07129 10.9708 2.05026 11.9497C3.02922 12.9287 4.2765 13.5954 5.63437 13.8655C6.99224 14.1356 8.3997 13.997 9.67879 13.4672C10.9579 12.9373 12.0511 12.0401 12.8203 10.889C13.5895 9.73785 14 8.38447 14 7C13.998 5.1441 13.2599 3.36479 11.9475 2.05247C10.6352 0.74015 8.8559 0.0020073 7 0ZM7.14583 2.91667C7.31889 2.91667 7.48807 2.96798 7.63196 3.06413C7.77585 3.16028 7.888 3.29693 7.95423 3.45682C8.02046 3.6167 8.03778 3.79264 8.00402 3.96237C7.97026 4.1321 7.88692 4.28801 7.76455 4.41038C7.64218 4.53276 7.48627 4.61609 7.31654 4.64985C7.14681 4.68362 6.97087 4.66629 6.81099 4.60006C6.6511 4.53383 6.51445 4.42168 6.4183 4.27779C6.32215 4.1339 6.27084 3.96472 6.27084 3.79167C6.27084 3.5596 6.36302 3.33704 6.52712 3.17295C6.69121 3.00885 6.91377 2.91667 7.14583 2.91667ZM8.45833 10.7917H6.125C5.97029 10.7917 5.82192 10.7302 5.71252 10.6208C5.60313 10.5114 5.54167 10.363 5.54167 10.2083C5.54167 10.0536 5.60313 9.90525 5.71252 9.79585C5.82192 9.68646 5.97029 9.625 6.125 9.625H6.5625C6.60118 9.625 6.63827 9.60963 6.66562 9.58228C6.69297 9.55494 6.70833 9.51784 6.70833 9.47917V6.85417C6.70833 6.81549 6.69297 6.7784 6.66562 6.75105C6.63827 6.7237 6.60118 6.70833 6.5625 6.70833H6.125C5.97029 6.70833 5.82192 6.64687 5.71252 6.53748C5.60313 6.42808 5.54167 6.27971 5.54167 6.125C5.54167 5.97029 5.60313 5.82192 5.71252 5.71252C5.82192 5.60312 5.97029 5.54167 6.125 5.54167H6.70833C7.01775 5.54167 7.3145 5.66458 7.53329 5.88337C7.75208 6.10217 7.875 6.39891 7.875 6.70833V9.47917C7.875 9.51784 7.89037 9.55494 7.91772 9.58228C7.94506 9.60963 7.98216 9.625 8.02083 9.625H8.45833C8.61304 9.625 8.76142 9.68646 8.87081 9.79585C8.98021 9.90525 9.04167 10.0536 9.04167 10.2083C9.04167 10.363 8.98021 10.5114 8.87081 10.6208C8.76142 10.7302 8.61304 10.7917 8.45833 10.7917Z" fill="#2C55FB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
3
web/public/icons/max-hourly-rate.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.65738 1.71633V0H15.3426V1.71633H8.65738ZM9.77159 16.3909L8.29526 13.3302C8.22098 13.1776 8.11421 13.0632 7.97493 12.9869C7.83565 12.9106 7.69174 12.8725 7.54318 12.8725H2C2.20427 10.2026 3.26277 7.95709 5.17549 6.13588C7.08821 4.31466 9.36305 3.40405 12 3.40405C13.2442 3.40405 14.4095 3.61383 15.4958 4.03337C16.5822 4.45292 17.5525 5.0441 18.4067 5.80691L19.8273 4.34803L21.0251 5.57807L19.6045 7.03695C20.1987 7.72348 20.7233 8.5435 21.1783 9.49702C21.6332 10.4505 21.9072 11.5757 22 12.8725H16.9861L14.9805 8.75328C14.8319 8.42908 14.5812 8.26698 14.2284 8.26698C13.8756 8.26698 13.6249 8.42908 13.4763 8.75328L9.77159 16.3909ZM12 24C9.36305 24 7.09285 23.0942 5.18941 21.2825C3.28598 19.4708 2.22284 17.2396 2 14.5888H7.01393L9.0195 18.708C9.16806 19.0322 9.41876 19.1943 9.77159 19.1943C10.1244 19.1943 10.3751 19.0322 10.5237 18.708L14.2284 11.0703L15.7047 14.1311C15.779 14.2837 15.8858 14.3981 16.0251 14.4744C16.1643 14.5507 16.3083 14.5888 16.4568 14.5888H22C21.7772 17.2396 20.714 19.4708 18.8106 21.2825C16.9071 23.0942 14.637 24 12 24Z" fill="#787A8A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
web/public/icons/max-pending.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 1.71633V0H15V1.71633H8ZM10.625 14.503H12.375V7.92372H10.625V14.503ZM11.5 24C10.0611 24 8.70486 23.7282 7.43125 23.1847C6.15764 22.6412 5.04444 21.9023 4.09167 20.9678C3.13889 20.0334 2.38542 18.9416 1.83125 17.6925C1.27708 16.4434 1 15.1132 1 13.702C1 12.2908 1.27708 10.9607 1.83125 9.71156C2.38542 8.46245 3.13889 7.37068 4.09167 6.43623C5.04444 5.50179 6.15764 4.76281 7.43125 4.21931C8.70486 3.6758 10.0611 3.40405 11.5 3.40405C12.8028 3.40405 14.0229 3.61383 15.1604 4.03337C16.2979 4.45292 17.3139 5.0441 18.2083 5.80691L19.6958 4.34803L20.95 5.57807L19.4625 7.03695C20.1625 7.79976 20.7604 8.72467 21.2562 9.81168C21.7521 10.8987 22 12.1955 22 13.702C22 15.1132 21.7229 16.4434 21.1687 17.6925C20.6146 18.9416 19.8611 20.0334 18.9083 20.9678C17.9556 21.9023 16.8424 22.6412 15.5687 23.1847C14.2951 23.7282 12.9389 24 11.5 24Z" fill="#787A8A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 966 B |
12
web/public/icons/minus.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#ffffff" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 459.313 459.314"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M459.313,229.648c0,22.201-17.992,40.199-40.205,40.199H40.181c-11.094,0-21.14-4.498-28.416-11.774
|
||||
C4.495,250.808,0,240.76,0,229.66c-0.006-22.204,17.992-40.199,40.202-40.193h378.936
|
||||
C441.333,189.472,459.308,207.456,459.313,229.648z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 708 B |
4
web/public/icons/mode.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#787A8A"/>
|
||||
<path d="M16.9671 10.1816C16.9423 10.1274 16.9049 10.0819 16.8588 10.0502C16.8128 10.0184 16.7599 10.0016 16.706 10.0016H12.3678L13.4641 4.40652C13.479 4.33143 13.4706 4.25281 13.4401 4.18399C13.4097 4.11517 13.3592 4.06041 13.2972 4.02901C13.2352 3.9976 13.1655 3.99148 13.1 4.0117C13.0345 4.03191 12.9771 4.07721 12.9377 4.1399L7.05621 13.4718C7.02424 13.5216 7.00509 13.5805 7.00088 13.642C6.99667 13.7034 7.00758 13.7649 7.03238 13.8198C7.05718 13.8746 7.09491 13.9206 7.14137 13.9526C7.18784 13.9846 7.24122 14.0013 7.29559 14.001H11.6338L10.5375 19.5935C10.5225 19.6686 10.531 19.7472 10.5614 19.816C10.5919 19.8848 10.6424 19.9396 10.7044 19.971C10.7664 20.0024 10.8361 20.0085 10.9016 19.9883C10.9671 19.9681 11.0245 19.9228 11.0639 19.8601L16.9454 10.5282C16.9768 10.4783 16.9954 10.4195 16.9993 10.3584C17.0031 10.2972 16.992 10.2361 16.9671 10.1816Z" fill="#090D2B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
3
web/public/icons/search-icon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6667 14.6663L12.5004 12.5M12.3138 6.8232C12.3138 9.85535 9.85572 12.3134 6.82357 12.3134C3.79142 12.3134 1.33337 9.85535 1.33337 6.8232C1.33337 3.79105 3.79142 1.33301 6.82357 1.33301C9.85572 1.33301 12.3138 3.79105 12.3138 6.8232Z" stroke="#787A8A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 434 B |
4
web/public/icons/sort-asc.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="7" height="13" viewBox="0 0 7 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 0.5L7 5.5H0L3.5 0.5Z" fill="#060814"/>
|
||||
<path d="M3.5 12.5L-5.38644e-08 7.5L7 7.5L3.5 12.5Z" fill="#CFD0DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 224 B |
4
web/public/icons/sort-desc.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="7" height="13" viewBox="0 0 7 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 0.5L7 5.5H0L3.5 0.5Z" fill="#CFD0DB"/>
|
||||
<path d="M3.5 12.5L-5.38644e-08 7.5L7 7.5L3.5 12.5Z" fill="#060814"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 224 B |
4
web/public/icons/sort.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="7" height="13" viewBox="0 0 7 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 0.5L7 5.5H0L3.5 0.5Z" fill="#CFD0DB"/>
|
||||
<path d="M3.5 12.5L-5.38644e-08 7.5L7 7.5L3.5 12.5Z" fill="#CFD0DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 224 B |
118
web/public/images/circuitbreaker-logo.svg
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_109_25712)">
|
||||
<mask id="mask0_109_25712" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_109_25712)">
|
||||
<g filter="url(#filter0_ddd_109_25712)">
|
||||
<path d="M0 0H44V44H0V0Z" fill="url(#paint0_linear_109_25712)"/>
|
||||
<path d="M0 0H44V44H0V0Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dddd_109_25712)">
|
||||
<path d="M27.2754 5.86902H16.6222C15.4782 5.86902 14.5508 6.79644 14.5508 7.94046V35.905C14.5508 37.049 15.4782 37.9765 16.6222 37.9765H27.2754C28.4194 37.9765 29.3468 37.049 29.3468 35.905V7.94046C29.3468 6.79644 28.4194 5.86902 27.2754 5.86902Z" fill="#F2FAFF"/>
|
||||
<path d="M27.2754 5.86902H16.6222C15.4782 5.86902 14.5508 6.79644 14.5508 7.94046V35.905C14.5508 37.049 15.4782 37.9765 16.6222 37.9765H27.2754C28.4194 37.9765 29.3468 37.049 29.3468 35.905V7.94046C29.3468 6.79644 28.4194 5.86902 27.2754 5.86902Z" fill="url(#paint1_linear_109_25712)" fill-opacity="0.1"/>
|
||||
<path d="M27.2754 5.86902H16.6222C15.4782 5.86902 14.5508 6.79644 14.5508 7.94046V35.905C14.5508 37.049 15.4782 37.9765 16.6222 37.9765H27.2754C28.4194 37.9765 29.3468 37.049 29.3468 35.905V7.94046C29.3468 6.79644 28.4194 5.86902 27.2754 5.86902Z" fill="url(#paint2_linear_109_25712)" fill-opacity="0.1"/>
|
||||
<path d="M27.2754 5.86902H16.6222C15.4782 5.86902 14.5508 6.79644 14.5508 7.94046V35.905C14.5508 37.049 15.4782 37.9765 16.6222 37.9765H27.2754C28.4194 37.9765 29.3468 37.049 29.3468 35.905V7.94046C29.3468 6.79644 28.4194 5.86902 27.2754 5.86902Z" fill="url(#paint3_linear_109_25712)" fill-opacity="0.39"/>
|
||||
<path d="M27.2754 6.02698H16.6222C15.5654 6.02698 14.7087 6.88367 14.7087 7.94046V35.905C14.7087 36.9618 15.5654 37.8185 16.6222 37.8185H27.2754C28.3322 37.8185 29.1889 36.9618 29.1889 35.905V7.94046C29.1889 6.88367 28.3322 6.02698 27.2754 6.02698Z" stroke="black" stroke-opacity="0.05" stroke-width="0.315921"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_ddd_109_25712)">
|
||||
<path d="M26.2266 13.5979L16.7817 23.0927H20.2849L17.5922 30.2965L27.2628 20.8012H23.759L26.2266 13.5979Z" fill="#FF8A00"/>
|
||||
<path d="M26.3761 13.6492L26.5984 13L26.1145 13.4865L16.6697 22.9813L16.4016 23.2508H16.7817H20.0571L17.4441 30.2412L17.1948 30.9081L17.7029 30.4093L27.3735 20.914L27.6493 20.6431H27.2628H23.9802L26.3761 13.6492Z" stroke="white" stroke-opacity="0.13" stroke-width="0.263268" stroke-miterlimit="4.62751"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ddd_109_25712" x="-3.07153" y="-5.31119" width="50.1431" height="54.0465" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.66374"/>
|
||||
<feGaussianBlur stdDeviation="1.53576"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.54 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_109_25712"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.23966"/>
|
||||
<feGaussianBlur stdDeviation="1.53576"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.577292 0 0 0 0 0.782759 0 0 0 0 0.85 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_109_25712" result="effect2_dropShadow_109_25712"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.831234"/>
|
||||
<feGaussianBlur stdDeviation="0.664987"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.858333 0 0 0 0 0.970486 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_109_25712" result="effect3_dropShadow_109_25712"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_109_25712" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_dddd_109_25712" x="12.5432" y="3.84118" width="18.8112" height="37.7505" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.14796"/>
|
||||
<feGaussianBlur stdDeviation="0.429789"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_109_25712"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.60762"/>
|
||||
<feGaussianBlur stdDeviation="1.00378"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_109_25712" result="effect2_dropShadow_109_25712"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.57721"/>
|
||||
<feGaussianBlur stdDeviation="0.225315"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.44 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_109_25712" result="effect3_dropShadow_109_25712"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.225315"/>
|
||||
<feGaussianBlur stdDeviation="0.168987"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.85 0"/>
|
||||
<feBlend mode="normal" in2="effect3_dropShadow_109_25712" result="effect4_dropShadow_109_25712"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_109_25712" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_ddd_109_25712" x="11.3461" y="7.76317" width="21.3639" height="28.3932" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="1.31634"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_109_25712"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2.36941"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.508167 0 0 0 0 0.0541667 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_109_25712" result="effect2_dropShadow_109_25712"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.526535"/>
|
||||
<feGaussianBlur stdDeviation="0.263268"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_109_25712" result="effect3_dropShadow_109_25712"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_109_25712" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_109_25712" x1="41.8303" y1="44.5432" x2="7.60245" y2="-2.75841" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D2DAE2"/>
|
||||
<stop offset="0.443664" stop-color="#EAF9FF"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_109_25712" x1="21.9488" y1="10.3488" x2="21.9488" y2="37.9765" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#25A4FF"/>
|
||||
<stop offset="1" stop-color="#057DD5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_109_25712" x1="22.1422" y1="5.9079" x2="19.7863" y2="37.7777" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_109_25712" x1="21.9488" y1="3.61587" x2="21.9488" y2="16.0082" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_109_25712">
|
||||
<rect width="44" height="44" rx="10.6562" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
BIN
web/public/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 101 KiB |
65
web/public/locales/en/common.json
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"cancel": "Cancel",
|
||||
"edit": "Edit",
|
||||
"footer": "2023 Bottlepay Ltd. All rights reserved.",
|
||||
"edit-defaults": "Edit defaults",
|
||||
"max-hourly-rate": "Max hourly rate",
|
||||
"max-pending": "Max pending",
|
||||
"mode": "Mode",
|
||||
"queue-alert": "If something goes wrong in queue mode, this may lead to channel force-closes in lnd 0.15 and below",
|
||||
"save": "Save changes",
|
||||
"unlimited": "Unlimited",
|
||||
"use-default-values": "Use default values",
|
||||
"blocked-description": "All traffic from this peer will be blocked completely",
|
||||
"node-key": "Node key",
|
||||
"node-version": "Node version",
|
||||
"errors": {
|
||||
"error-updating-selected": "Error updating limit for selected peers",
|
||||
"error-updating-default": "Error updating default limits",
|
||||
"error-fetching-limits": "Error fetching limits data",
|
||||
"error-fetching-info": "Error fetching node info"
|
||||
},
|
||||
"default-limit-modal": {
|
||||
"title": "Default limits"
|
||||
},
|
||||
"modes": {
|
||||
"MODE_FAIL": "Fail",
|
||||
"MODE_QUEUE": "Queue",
|
||||
"MODE_QUEUE_PEER_INITIATED": "Queue Peer Initiated",
|
||||
"MODE_BLOCK": "Block"
|
||||
},
|
||||
"node-table": {
|
||||
"MODE_FAIL": "Fail",
|
||||
"MODE_QUEUE": "Queue",
|
||||
"MODE_QUEUE_PEER_INITIATED": "Queue Peer Initiated",
|
||||
"MODE_BLOCK": "Block",
|
||||
"search-placeholder": "Search peer...",
|
||||
"columns": "Columns",
|
||||
"edit-selected": "Edit selected",
|
||||
"alias": "Peer",
|
||||
"default": "Default",
|
||||
"currentFwds": "Current fwds",
|
||||
"queueLen": "Queue Len",
|
||||
"pendingHtlcCount": "Pending",
|
||||
"edit-selected-nodes": "Edit selected nodes",
|
||||
"selected-peers": "Selected peers",
|
||||
"counter1h": {
|
||||
"title": "1 hour counters",
|
||||
"fail": "Fail",
|
||||
"success": "OK",
|
||||
"reject": "Reject"
|
||||
},
|
||||
"counter24h": {
|
||||
"title": "24 hour counters",
|
||||
"fail": "Fail",
|
||||
"success": "OK",
|
||||
"reject": "Reject"
|
||||
},
|
||||
"limit": {
|
||||
"title": "Limits",
|
||||
"maxPending": "Max Pending",
|
||||
"maxHourlyRate": "Max Hourly Rate",
|
||||
"mode": "Mode"
|
||||
}
|
||||
}
|
||||
}
|
||||
86
web/src/components/Checkbox/Checkbox.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import * as React from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MuiCheckbox, {
|
||||
CheckboxProps as MuiCheckboxProps,
|
||||
} from '@mui/material/Checkbox';
|
||||
import { get } from 'lodash';
|
||||
|
||||
interface StyleProps {
|
||||
variant: 'primary' | 'secondary';
|
||||
}
|
||||
|
||||
const BpIcon = styled('span')<StyleProps>(({ theme }) => ({
|
||||
borderRadius: 4,
|
||||
width: 12,
|
||||
height: 12,
|
||||
border: `1px solid`,
|
||||
borderColor: `#C5C7D6`,
|
||||
backgroundColor: theme.palette.grey['50'],
|
||||
position: 'relative',
|
||||
'.Mui-focusVisible &': {
|
||||
boxShadow: '0 0 0 2px #E8ECFF',
|
||||
},
|
||||
'input:hover ~ &': {
|
||||
boxShadow: '0 0 0 2px #E8ECFF',
|
||||
},
|
||||
'input:disabled ~ &': {
|
||||
boxShadow: 'none',
|
||||
background: '#E4E6EE',
|
||||
borderColor: '#E8ECFF',
|
||||
},
|
||||
}));
|
||||
|
||||
const variants = {
|
||||
primary: {
|
||||
backgroundColor: 'primary.main',
|
||||
iconName: 'check',
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: 'grey.50',
|
||||
iconName: 'check-primary',
|
||||
},
|
||||
};
|
||||
const BpCheckedIcon = styled(BpIcon)(({ theme, variant }) => ({
|
||||
backgroundColor: get(theme.palette, variants[variant].backgroundColor),
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.primary.main,
|
||||
'&:before': {
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundImage: `url(/icons/${variants[variant].iconName}.svg)`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '8px 8px',
|
||||
backgroundPosition: 'center',
|
||||
content: '""',
|
||||
},
|
||||
}));
|
||||
|
||||
const BpIndeterminateIcon = styled(BpCheckedIcon)({
|
||||
'&:before': {
|
||||
backgroundImage: 'url(/icons/minus.svg)',
|
||||
},
|
||||
});
|
||||
|
||||
interface CheckboxProps extends MuiCheckboxProps, Partial<StyleProps> {}
|
||||
|
||||
// Inspired by blueprintjs
|
||||
const Checkbox = ({ variant = 'primary', ...rest }: CheckboxProps) => (
|
||||
<MuiCheckbox
|
||||
disableRipple
|
||||
color="default"
|
||||
checkedIcon={<BpCheckedIcon variant={variant} />}
|
||||
icon={<BpIcon variant={variant} />}
|
||||
indeterminateIcon={<BpIndeterminateIcon variant={variant} />}
|
||||
inputProps={{ 'aria-label': 'Checkbox' }}
|
||||
sx={{
|
||||
'&:hover': { bgcolor: 'transparent' },
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Checkbox;
|
||||
1
web/src/components/Checkbox/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Checkbox';
|
||||
276
web/src/components/EditLimitsForm/EditLimitsForm.tsx
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Box,
|
||||
Select,
|
||||
MenuItem,
|
||||
InputLabel,
|
||||
InputBase,
|
||||
Typography,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
|
||||
import { Mode } from 'enums';
|
||||
import { Checkbox, PrimaryButton, SecondaryButton } from 'components';
|
||||
import { useLimits } from 'hooks';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface EditLimitsFormProps {
|
||||
initialValues?: Limit;
|
||||
onCancel: () => void;
|
||||
onSubmit: (newLimit: Limit) => void;
|
||||
isNodeEdit?: boolean;
|
||||
}
|
||||
|
||||
const EditLimitsForm = ({
|
||||
initialValues,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
isNodeEdit,
|
||||
}: EditLimitsFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useLimits();
|
||||
|
||||
const [maxHourlyRate, setMaxHourlyRate] = useState(
|
||||
initialValues?.maxHourlyRate || ''
|
||||
);
|
||||
const [maxPending, setMaxPending] = useState(initialValues?.maxPending || '');
|
||||
const [mode, setMode] = useState<Mode>(initialValues?.mode || Mode.Fail);
|
||||
|
||||
const [isUseDefaultValues, setIsUseDefaultValues] = useState(false);
|
||||
|
||||
const [isMaxHourlyRateUnlimited, setIsMaxHourlyRateUnlimited] = useState(
|
||||
initialValues?.maxHourlyRate === '0'
|
||||
);
|
||||
const [isMaxPendingUnlimited, setIsMaxPendingUnlimited] = useState(
|
||||
initialValues?.maxPending === '0'
|
||||
);
|
||||
|
||||
const handleUseDefaultValuesChecked = (checked: boolean) => {
|
||||
setIsUseDefaultValues(checked);
|
||||
const { defaultLimit } = data!;
|
||||
if (checked) {
|
||||
setMaxHourlyRate(defaultLimit.maxHourlyRate);
|
||||
if (defaultLimit.maxHourlyRate === '0') setIsMaxHourlyRateUnlimited(true);
|
||||
setMaxPending(defaultLimit.maxPending);
|
||||
if (defaultLimit.maxPending === '0') setIsMaxPendingUnlimited(true);
|
||||
setMode(defaultLimit.mode);
|
||||
} else {
|
||||
setMaxHourlyRate('');
|
||||
setIsMaxHourlyRateUnlimited(false);
|
||||
setMaxPending('');
|
||||
setIsMaxPendingUnlimited(false);
|
||||
setMode(Mode.Fail);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit({ maxHourlyRate, maxPending, mode });
|
||||
};
|
||||
|
||||
const isModeBlock = mode === Mode.Block;
|
||||
const isModeQueue = mode === Mode.Queue || mode === Mode.QueuePeerInitiated;
|
||||
|
||||
const isUnlimited = isMaxHourlyRateUnlimited && isMaxPendingUnlimited;
|
||||
|
||||
const isButtonDisabled =
|
||||
!maxHourlyRate ||
|
||||
!maxPending ||
|
||||
!mode ||
|
||||
(initialValues &&
|
||||
maxHourlyRate === initialValues.maxHourlyRate &&
|
||||
maxPending === initialValues.maxPending &&
|
||||
mode === initialValues.mode);
|
||||
|
||||
const handleModeChange = (value: Mode) => {
|
||||
setMode(value as Mode);
|
||||
if (value === Mode.Block) {
|
||||
if (!maxHourlyRate) {
|
||||
setMaxHourlyRate(data!.defaultLimit.maxHourlyRate);
|
||||
if (data!.defaultLimit.maxHourlyRate === '0')
|
||||
setIsMaxHourlyRateUnlimited(true);
|
||||
}
|
||||
if (!maxPending) {
|
||||
setMaxPending(data!.defaultLimit.maxPending);
|
||||
if (data!.defaultLimit.maxPending === '0')
|
||||
setIsMaxPendingUnlimited(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getMaskedLimit = (value: string) => {
|
||||
if (isModeBlock) return '0';
|
||||
return value === '0' ? '∞' : value;
|
||||
};
|
||||
|
||||
const getSelectValue = () => {
|
||||
if (mode === Mode.Block) return mode;
|
||||
return isUnlimited ? 'allow-all' : mode;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box component="form" onSubmit={handleSubmit}>
|
||||
{isNodeEdit && (
|
||||
<InputLabel
|
||||
sx={{
|
||||
backgroundColor: '#E4E6EE',
|
||||
p: 3,
|
||||
color: '#5C6484',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
mb: 4,
|
||||
...(isUseDefaultValues && {
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'grey.50',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
variant="secondary"
|
||||
value={isUseDefaultValues}
|
||||
onChange={(e) => handleUseDefaultValuesChecked(e.target.checked)}
|
||||
sx={{
|
||||
mr: 2,
|
||||
'input:hover ~ span': {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{t('use-default-values')}
|
||||
</InputLabel>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<Box sx={{ flex: 1, mb: 4 }}>
|
||||
<Box sx={{ mb: '6px' }}>
|
||||
<InputLabel>{t('max-hourly-rate')}</InputLabel>
|
||||
<InputBase
|
||||
fullWidth
|
||||
type={maxHourlyRate !== '0' ? 'number' : 'text'}
|
||||
value={getMaskedLimit(maxHourlyRate)}
|
||||
inputProps={{ min: '1' }}
|
||||
onChange={(e) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = e;
|
||||
if (value === '0') setIsMaxHourlyRateUnlimited(true);
|
||||
setMaxHourlyRate(value);
|
||||
}}
|
||||
disabled={
|
||||
isMaxHourlyRateUnlimited || isUseDefaultValues || isModeBlock
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<InputLabel
|
||||
disabled={isUseDefaultValues || isModeBlock}
|
||||
sx={{ mb: 0 }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isMaxHourlyRateUnlimited}
|
||||
onChange={(e) => {
|
||||
setIsMaxHourlyRateUnlimited(e.target.checked);
|
||||
if (e.target.checked) setMaxHourlyRate('0');
|
||||
else setMaxHourlyRate('');
|
||||
}}
|
||||
disabled={isUseDefaultValues || isModeBlock}
|
||||
sx={{ mr: '6px' }}
|
||||
/>
|
||||
{t('unlimited')}
|
||||
</InputLabel>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box sx={{ mb: '6px' }}>
|
||||
<InputLabel>{t('max-pending')}</InputLabel>
|
||||
<InputBase
|
||||
fullWidth
|
||||
type={maxPending !== '0' ? 'number' : 'text'}
|
||||
value={getMaskedLimit(maxPending)}
|
||||
inputProps={{ min: '1' }}
|
||||
onChange={(e) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = e;
|
||||
setMaxPending(value);
|
||||
if (value === '0') setIsMaxPendingUnlimited(true);
|
||||
}}
|
||||
disabled={
|
||||
isMaxPendingUnlimited || isUseDefaultValues || isModeBlock
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<InputLabel
|
||||
disabled={isUseDefaultValues || isModeBlock}
|
||||
sx={{ mb: 0 }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isMaxPendingUnlimited}
|
||||
onChange={(e) => {
|
||||
setIsMaxPendingUnlimited(e.target.checked);
|
||||
if (e.target.checked) setMaxPending('0');
|
||||
else setMaxPending('');
|
||||
}}
|
||||
disabled={isUseDefaultValues || isModeBlock}
|
||||
sx={{ mr: '6px' }}
|
||||
/>
|
||||
{t('unlimited')}
|
||||
</InputLabel>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<InputLabel>{t('mode')}</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
input={<InputBase />}
|
||||
value={getSelectValue()}
|
||||
fullWidth
|
||||
disabled={(mode !== Mode.Block && isUnlimited) || isUseDefaultValues}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
handleModeChange(e.target.value as Mode);
|
||||
}}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{!isModeBlock && isUnlimited && (
|
||||
<MenuItem value="allow-all">-</MenuItem>
|
||||
)}
|
||||
{Object.entries(Mode).map(([key, value]) => (
|
||||
<MenuItem value={value} key={key}>
|
||||
{t(`modes.${value}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{isModeBlock && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ mr: '6px', img: { display: 'block' } }}>
|
||||
<Image src="/icons/info.svg" width="14" height="14" alt="info" />
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '10px', color: '#5C6484' }}>
|
||||
{t('blocked-description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{isModeQueue && (
|
||||
<Box>
|
||||
<Alert severity="warning">{t('queue-alert')}</Alert>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<PrimaryButton fullWidth type="submit" disabled={isButtonDisabled}>
|
||||
{t('save')}
|
||||
</PrimaryButton>
|
||||
<SecondaryButton fullWidth type="button" onClick={onCancel}>
|
||||
{t('cancel')}
|
||||
</SecondaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditLimitsForm;
|
||||
1
web/src/components/EditLimitsForm/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditLimitsForm';
|
||||
32
web/src/components/Fonts/Fonts.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { css, Global } from '@emotion/react';
|
||||
|
||||
export interface FontsProps {
|
||||
fontDisplay?: 'auto' | 'block' | 'fallback' | 'optional' | 'swap';
|
||||
}
|
||||
|
||||
const Fonts = ({ fontDisplay = 'auto' }: FontsProps) => (
|
||||
<Global
|
||||
styles={css`
|
||||
@font-face {
|
||||
font-family: 'Helvetica Neue';
|
||||
src: url('/fonts/HelveticaNeue.eot') format('embedded-opentype'),
|
||||
url('/fonts/HelveticaNeue.woff2') format('woff2'),
|
||||
url('/fonts/HelveticaNeue.woff') format('woff'),
|
||||
url('/fonts/HelveticaNeue.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-display: ${fontDisplay};
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Helvetica Neue';
|
||||
src: url('/fonts/HelveticaNeueBold.eot') format('embedded-opentype'),
|
||||
url('/fonts/HelveticaNeueBold.woff2') format('woff2'),
|
||||
url('/fonts/HelveticaNeueBold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-display: ${fontDisplay};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Fonts;
|
||||
1
web/src/components/Fonts/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Fonts';
|
||||
26
web/src/components/Modal/Modal.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Dialog, DialogTitle, DialogProps, Drawer } from '@mui/material';
|
||||
|
||||
const Modal = ({
|
||||
title,
|
||||
children,
|
||||
...rest
|
||||
}: React.PropsWithChildren<DialogProps>) => (
|
||||
<>
|
||||
<Dialog sx={{ display: { xs: 'none', md: 'initial' } }} {...rest}>
|
||||
<DialogTitle sx={{ p: 0, mb: 4 }}>{title}</DialogTitle>
|
||||
{children}
|
||||
</Dialog>
|
||||
<Drawer
|
||||
anchor="bottom"
|
||||
sx={{
|
||||
display: { xs: 'initial', md: 'none' },
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<DialogTitle sx={{ p: 0, mb: 4 }}>{title}</DialogTitle>
|
||||
{children}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
|
||||
export default Modal;
|
||||
1
web/src/components/Modal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Modal';
|
||||
12
web/src/components/NodeAlias/NodeAlias.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
interface NodeAliasProps {
|
||||
alias?: string;
|
||||
nodeLimit: NodeLimit;
|
||||
}
|
||||
|
||||
const NodeAlias = ({ alias, nodeLimit }: NodeAliasProps) => (
|
||||
<>
|
||||
{alias || `${nodeLimit.node.slice(0, 8)}...${nodeLimit.node.slice(58, 66)}`}
|
||||
</>
|
||||
);
|
||||
|
||||
export default NodeAlias;
|
||||
1
web/src/components/NodeAlias/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './NodeAlias';
|
||||
18
web/src/components/PrimaryButton/PrimaryButton.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Button, ButtonProps } from '@mui/material';
|
||||
|
||||
const PrimaryButton = ({ sx, ...rest }: ButtonProps) => (
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'grey.50',
|
||||
height: '39px',
|
||||
'&:hover': {
|
||||
backgroundColor: '#387AFF',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export default PrimaryButton;
|
||||
1
web/src/components/PrimaryButton/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './PrimaryButton';
|
||||
23
web/src/components/SecondaryButton/SecondaryButton.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Button, ButtonProps } from '@mui/material';
|
||||
|
||||
const SecondaryButton = ({ sx, ...rest }: ButtonProps) => (
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: 'grey.100',
|
||||
color: '#5C6484',
|
||||
border: '1px solid',
|
||||
borderColor: '#D3D4DB',
|
||||
height: '39px',
|
||||
'&:hover, &:focus': {
|
||||
borderColor: '#C5C7D6',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: '#F2F2F7',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export default SecondaryButton;
|
||||
1
web/src/components/SecondaryButton/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SecondaryButton';
|
||||
59
web/src/components/Snackbar/ErrorSnackbar.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
|
||||
import { closeSnackbar, CustomContentProps, SnackbarContent } from 'notistack';
|
||||
|
||||
const ErrorSnackbar = forwardRef<HTMLDivElement, CustomContentProps>(
|
||||
({ message, id }, ref) => (
|
||||
<SnackbarContent ref={ref}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
width: { xs: '100%', md: '325px' },
|
||||
height: '52px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'error.main',
|
||||
p: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderTopLeftRadius: '8px',
|
||||
borderBottomLeftRadius: '8px',
|
||||
'> img': { display: 'block', width: '20px', height: '20px' },
|
||||
}}
|
||||
>
|
||||
<img src="/icons/exclamation.svg" alt="error" />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
p: 4,
|
||||
backgroundColor: 'grey.50',
|
||||
border: '1px solid',
|
||||
borderLeft: 'none',
|
||||
borderColor: 'grey.400',
|
||||
borderTopRightRadius: '8px',
|
||||
borderBottomRightRadius: '8px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography>{message}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ pl: 4 }}>
|
||||
<IconButton type="button" onClick={() => closeSnackbar(id)}>
|
||||
<img src="/icons/close.svg" alt="close" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</SnackbarContent>
|
||||
)
|
||||
);
|
||||
|
||||
export default ErrorSnackbar;
|
||||
1
web/src/components/Snackbar/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as ErrorSnackbar } from './ErrorSnackbar';
|
||||
8
web/src/components/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { default as Checkbox } from './Checkbox';
|
||||
export { default as EditLimitsForm } from './EditLimitsForm';
|
||||
export { default as Fonts } from './Fonts';
|
||||
export { default as PrimaryButton } from './PrimaryButton';
|
||||
export { default as Modal } from './Modal';
|
||||
export { default as NodeAlias } from './NodeAlias';
|
||||
export { default as SecondaryButton } from './SecondaryButton';
|
||||
export * from './Snackbar';
|
||||
6
web/src/config/config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export const getConfig = (key?: keyof Config) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return key ? window.config[key] : window.config;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
1
web/src/config/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { getConfig } from './config';
|
||||
1
web/src/constant/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './style';
|
||||
3
web/src/constant/style.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const HEADER_HEIGHT_DESKTOP = '80px';
|
||||
export const HEADER_HEIGHT_MOBILE = '158px';
|
||||
export const FOOTER_HEIGHT = '63px';
|
||||
6
web/src/enums/circuitbreaker.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export enum Mode {
|
||||
Fail = 'MODE_FAIL',
|
||||
Queue = 'MODE_QUEUE',
|
||||
QueuePeerInitiated = 'MODE_QUEUE_PEER_INITIATED',
|
||||
Block = 'MODE_BLOCK',
|
||||
}
|
||||
2
web/src/enums/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './circuitbreaker';
|
||||
export * from './routes';
|
||||
3
web/src/enums/routes.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export enum Route {
|
||||
Home = '/',
|
||||
}
|
||||
17
web/src/global.css
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: #101329;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
body > div:first-of-type,
|
||||
div#__next {
|
||||
height: 100dvh;
|
||||
width: 100dvw;
|
||||
overflow: hidden;
|
||||
}
|
||||
2
web/src/hooks/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as useInfo } from './useInfo';
|
||||
export { default as useLimits } from './useLimits';
|
||||
25
web/src/hooks/useInfo.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import { getInfo } from 'services/circuitbreaker';
|
||||
|
||||
const useInfo = () => {
|
||||
const { t: tError } = useTranslation('common', { keyPrefix: 'errors' });
|
||||
|
||||
const query = useQuery<Info, AxiosError<APIError>>({
|
||||
queryKey: ['info'],
|
||||
queryFn: getInfo,
|
||||
staleTime: 0,
|
||||
retry: false,
|
||||
onError: () =>
|
||||
enqueueSnackbar(tError('error-fetching-info'), {
|
||||
variant: 'error',
|
||||
}),
|
||||
});
|
||||
|
||||
return { info: { ...query.data }, ...query };
|
||||
};
|
||||
|
||||
export default useInfo;
|
||||
26
web/src/hooks/useLimits.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getLimits } from 'services/circuitbreaker';
|
||||
|
||||
const useLimits = () => {
|
||||
const { t: tError } = useTranslation('common', { keyPrefix: 'errors' });
|
||||
|
||||
const query = useQuery<Limits, AxiosError<APIError>>({
|
||||
queryKey: ['limits'],
|
||||
queryFn: getLimits,
|
||||
staleTime: 0,
|
||||
refetchInterval: 30000,
|
||||
retry: false,
|
||||
onError: () =>
|
||||
enqueueSnackbar(tError('error-fetching-limits'), {
|
||||
variant: 'error',
|
||||
}),
|
||||
});
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export default useLimits;
|
||||
53
web/src/pages/_app.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import 'normalize.css';
|
||||
|
||||
import 'global.css';
|
||||
import { QueryClientProvider } from 'providers';
|
||||
import { Fonts, ErrorSnackbar } from 'components';
|
||||
import customTheme from 'theme';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import i18n from '../../i18n';
|
||||
|
||||
const theme = createTheme(customTheme);
|
||||
|
||||
// Only render i18n provider on client side since it is a static webapp
|
||||
const ClientI18nextProvider = ({ children }: React.PropsWithChildren<{}>) =>
|
||||
typeof window !== undefined ? (
|
||||
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps) => (
|
||||
<>
|
||||
<Head>
|
||||
{/* <meta name="viewport" content="initial-scale=1, width=device-width" /> */}
|
||||
<title>Circuit Breaker ⚡️</title>
|
||||
<meta name="description" content="Advanced Lightning Node protection" />
|
||||
</Head>
|
||||
<ClientI18nextProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<SnackbarProvider
|
||||
maxSnack={2}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
autoHideDuration={6000}
|
||||
Components={{
|
||||
error: ErrorSnackbar,
|
||||
}}
|
||||
>
|
||||
<QueryClientProvider>
|
||||
<Fonts />
|
||||
<Component {...pageProps} />
|
||||
</QueryClientProvider>
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</ClientI18nextProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
export default App;
|
||||
40
web/src/pages/_document.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { loaderStyles } from 'splashScreen';
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
{/* <script type="text/javascript" src="/config.js" /> */}
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="/images/favicon.ico"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="image"
|
||||
href="/images/circuitbreaker-logo.svg"
|
||||
/>
|
||||
<style>{loaderStyles}</style>
|
||||
</Head>
|
||||
<body>
|
||||
<div id="globalLoader">
|
||||
<img
|
||||
className="globalLoaderSpinner"
|
||||
src="/images/circuitbreaker-logo.svg"
|
||||
height="40"
|
||||
width="40"
|
||||
alt="loader"
|
||||
/>
|
||||
</div>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
||||
5
web/src/pages/index.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Home } from 'views';
|
||||
|
||||
const HomePage = () => <Home />;
|
||||
|
||||
export default HomePage;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const BottleQueryClientProvider = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
export default BottleQueryClientProvider;
|
||||
1
web/src/providers/QueryClientProvider/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './QueryClientProvider';
|
||||
1
web/src/providers/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as QueryClientProvider } from './QueryClientProvider';
|
||||
31
web/src/services/circuitbreaker.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const circuitbreakerApi = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
export const getInfo = async () =>
|
||||
(await circuitbreakerApi.get<Info>('/info')).data;
|
||||
|
||||
export const getLimits = async () =>
|
||||
(await circuitbreakerApi.get<Limits>('/limits')).data;
|
||||
|
||||
interface UpdateLimitsParams {
|
||||
limits: {
|
||||
[key: string]: Limit;
|
||||
};
|
||||
}
|
||||
export const updateLimits = async (params: UpdateLimitsParams) =>
|
||||
(await circuitbreakerApi.post<{}>('/updatelimits', params)).data;
|
||||
|
||||
interface ClearLimitsParams {
|
||||
nodes: string[];
|
||||
}
|
||||
export const clearLimits = async (params: ClearLimitsParams) =>
|
||||
(await circuitbreakerApi.post<{}>('/clearlimits', params)).data;
|
||||
|
||||
interface UpdateDefaultLimitParams {
|
||||
limit: Limit;
|
||||
}
|
||||
export const updateDefaultLimit = async (params: UpdateDefaultLimitParams) =>
|
||||
(await circuitbreakerApi.post<{}>('/updatedefaultlimit', params)).data;
|
||||
2
web/src/splashScreen/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as loaderStyles } from './loaderStyles';
|
||||
export { default as removeLoader } from './removeLoader';
|
||||
18
web/src/splashScreen/loaderStyles.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const loaderStyles = `
|
||||
#globalLoader {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: background: linear-gradient(0deg, #0e101b -19.82%, #060712 64.16%), #ffffff;
|
||||
transition: opacity 250ms ease-in-out 0ms;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default loaderStyles;
|
||||
14
web/src/splashScreen/removeLoader.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const removeLoader = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const loader = document.getElementById('globalLoader');
|
||||
if (loader) {
|
||||
loader.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
loader.style.display = 'none';
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default removeLoader;
|
||||
11
web/src/theme/breakpoints.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const breakpoints = {
|
||||
values: {
|
||||
xs: 0,
|
||||
sm: 376,
|
||||
md: 600,
|
||||
lg: 960,
|
||||
xl: 1280,
|
||||
},
|
||||
};
|
||||
|
||||
export default breakpoints;
|
||||
345
web/src/theme/components.ts
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
import { Theme } from '@mui/material';
|
||||
|
||||
const components: Theme['components'] = {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
textTransform: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
px: 4,
|
||||
py: 3,
|
||||
'&:disabled': {
|
||||
backgroundColor: '#E4EAFF',
|
||||
color: '#828CC5',
|
||||
border: 'none',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTable: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => theme.unstable_sx({}),
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'.MuiTableRow-root': {
|
||||
':first-of-type': {
|
||||
'.MuiTableCell-root': {
|
||||
borderTopStyle: 'solid',
|
||||
':first-of-type': {
|
||||
borderTopLeftRadius: '12px',
|
||||
},
|
||||
':last-of-type': {
|
||||
borderTopRightRadius: '12px',
|
||||
},
|
||||
},
|
||||
},
|
||||
':last-of-type': {
|
||||
'.MuiTableCell-root': {
|
||||
':last-of-type': {
|
||||
borderBottomRightRadius: '12px',
|
||||
},
|
||||
},
|
||||
},
|
||||
'.MuiTableCell-root': {
|
||||
':first-of-type': {
|
||||
borderLeftStyle: 'solid',
|
||||
},
|
||||
borderRightStyle: 'solid',
|
||||
borderBottomStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
backgroundColor: 'grey.100',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTableBody: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'.MuiTableRow-root:nth-of-type(even):not(.Mui-selected)': {
|
||||
backgroundColor: 'grey.200',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTableContainer: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
borderTopLeftRadius: '12px',
|
||||
borderTopRightRadius: '12px',
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: '#EBEFFF',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: '#E5E6EE !important',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
whiteSpace: 'nowrap',
|
||||
borderColor: 'grey.300',
|
||||
p: 3,
|
||||
fontSize: '12px',
|
||||
color: 'grey.900',
|
||||
lineHeight: 1.25,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiModal: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'.MuiDialog-paper': {
|
||||
backgroundColor: 'grey.100',
|
||||
p: 6,
|
||||
borderRadius: '8px',
|
||||
width: '325px',
|
||||
m: 0,
|
||||
},
|
||||
'.MuiDrawer-paper': {
|
||||
borderTopLeftRadius: '8px',
|
||||
borderTopRightRadius: '8px',
|
||||
backgroundColor: 'grey.100',
|
||||
p: 4,
|
||||
pb: 8,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiInputLabel: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
fontWeight: 500,
|
||||
color: '#5C6484',
|
||||
mb: '6px',
|
||||
'&.Mui-disabled': {
|
||||
color: '#9DA3C5',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'.MuiInputBase-input': {
|
||||
fontSize: 16,
|
||||
py: 0,
|
||||
lineHeight: '23px',
|
||||
'::placeholder': {
|
||||
color: '#6E6F78 !important',
|
||||
opacity: 1,
|
||||
},
|
||||
'&.MuiInputBase-inputAdornedStart': {
|
||||
pl: 2,
|
||||
},
|
||||
},
|
||||
height: '39px',
|
||||
px: 4,
|
||||
py: 2,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#FFFFFF',
|
||||
border: '1px solid #D3D4DB',
|
||||
'&:not(.Mui-disabled)': {
|
||||
'&:hover, &.Mui-focused': {
|
||||
borderColor: '#C5C7D6',
|
||||
boxShadow: '0 0 0 2px #E8ECFF',
|
||||
},
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: '#E4E6EE',
|
||||
borderColor: '#E4E6EE',
|
||||
'.MuiInputBase-input': {
|
||||
color: '#9DA3C5',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiCheckbox: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
backgroundColor: 'grey.50',
|
||||
borderRadius: '8px !important',
|
||||
padding: '0 !important',
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiDivider: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
borderColor: '#D3D4DB',
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
py: 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiListItemText: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
color: '#474A59',
|
||||
}),
|
||||
},
|
||||
},
|
||||
// ACCORDION STYLES
|
||||
MuiAccordion: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
p: 0,
|
||||
backgroundColor: 'transparent',
|
||||
boxShadow: 'none',
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiAccordionSummary: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
p: 0,
|
||||
justifyContent: 'flex-start',
|
||||
minHeight: '0 !important',
|
||||
'.MuiAccordionSummary-content, .MuiAccordionSummary-content.Mui-expanded':
|
||||
{
|
||||
m: 0,
|
||||
flexGrow: 0,
|
||||
mr: 1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiAccordionDetails: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
p: 0,
|
||||
pt: 2,
|
||||
}),
|
||||
},
|
||||
},
|
||||
// SELECT STYLES
|
||||
MuiPopover: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'.MuiPopover-paper': {
|
||||
background: '#F6F6FA',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
'.MuiMenu-list': {
|
||||
p: 0,
|
||||
'.MuiMenuItem-root': {
|
||||
px: 4,
|
||||
py: 3,
|
||||
fontSize: '16px',
|
||||
'&:not(:last-of-type)': {
|
||||
borderBottom: '1px solid #D3D4DB',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTablePagination: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'.MuiToolbar-root': {
|
||||
pl: 0,
|
||||
minHeight: '0',
|
||||
mt: 6,
|
||||
},
|
||||
'.MuiTablePagination-selectLabel': {
|
||||
fontSize: '12px',
|
||||
m: 0,
|
||||
mr: 2,
|
||||
},
|
||||
'.MuiTablePagination-select': {
|
||||
fontSize: '12px',
|
||||
},
|
||||
'.MuiTablePagination-displayedRows': {
|
||||
minWidth: '20px',
|
||||
fontSize: '12px',
|
||||
order: 4,
|
||||
m: 0,
|
||||
},
|
||||
'.MuiTablePagination-actions': {
|
||||
ml: 0,
|
||||
mr: 2,
|
||||
},
|
||||
'.MuiInputBase-root': {
|
||||
height: '26px',
|
||||
px: 2,
|
||||
minWidth: '70px',
|
||||
m: 0,
|
||||
mr: 2,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
popper: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
'.MuiTooltip-tooltip': {
|
||||
backgroundColor: '#F6F6FA',
|
||||
p: 4,
|
||||
borderRadius: '8px',
|
||||
boxShadow:
|
||||
'0px 2px 2px rgba(0, 0, 0, 0.12), 0px 4px 12px rgba(0, 0, 0, 0.16)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) =>
|
||||
theme.unstable_sx({
|
||||
borderRadius: '8px',
|
||||
p: 4,
|
||||
'&.MuiAlert-standardWarning': {
|
||||
backgroundColor: 'warning.light',
|
||||
},
|
||||
'.MuiAlert-icon': {
|
||||
display: 'none',
|
||||
},
|
||||
'.MuiAlert-message': {
|
||||
p: 0,
|
||||
color: 'grey.900',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default components;
|
||||
15
web/src/theme/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import breakpoints from './breakpoints';
|
||||
import components from './components';
|
||||
import palette from './palette';
|
||||
import spacing from './spacing';
|
||||
import typography from './typography';
|
||||
|
||||
export default {
|
||||
breakpoints,
|
||||
components,
|
||||
palette,
|
||||
spacing,
|
||||
typography,
|
||||
};
|
||||
|
||||
export type Palette = typeof palette;
|
||||
14
web/src/theme/palette.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export default {
|
||||
primary: { light: '#EBEFFF', main: '#2C55FB', dark: '#101329' },
|
||||
error: { light: '#F4E6ED', main: '#DA2F58' },
|
||||
warning: { light: '#FFF7E0', main: '#FFBF00' },
|
||||
grey: {
|
||||
50: '#FFFFFF',
|
||||
100: '#F6F6FA',
|
||||
200: '#F6F6FAB3',
|
||||
300: '#E5E6EE',
|
||||
400: '#D3D4DB',
|
||||
800: '#6E6F78',
|
||||
900: '#060814',
|
||||
},
|
||||
};
|
||||
3
web/src/theme/spacing.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const spacing = 4;
|
||||
|
||||
export default spacing;
|
||||
22
web/src/theme/typography.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const typography = {
|
||||
fontFamily: '"Helvetica Neue", sans-serif',
|
||||
h3: {
|
||||
fontSize: '20px',
|
||||
lineHeight: '24px',
|
||||
color: 'grey.900',
|
||||
},
|
||||
h5: {
|
||||
fontSize: '12px',
|
||||
lineHeight: '15px',
|
||||
color: 'grey.900',
|
||||
fontWeight: '700',
|
||||
},
|
||||
body1: {
|
||||
color: '#060814',
|
||||
fontSize: '12px',
|
||||
lineHeight: '15px',
|
||||
fontWeight: '500',
|
||||
},
|
||||
};
|
||||
|
||||
export default typography;
|
||||
50
web/src/types/circuitbreaker.d.ts
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
interface Info {
|
||||
version: string;
|
||||
nodeKey: string;
|
||||
nodeAlias: string;
|
||||
nodeVersion: string;
|
||||
}
|
||||
|
||||
interface Counter {
|
||||
fail: number;
|
||||
success: number;
|
||||
reject: number;
|
||||
}
|
||||
|
||||
interface Limit {
|
||||
maxHourlyRate: string;
|
||||
maxPending: string;
|
||||
mode: Mode;
|
||||
}
|
||||
|
||||
interface NodeLimit {
|
||||
node: string;
|
||||
alias: string;
|
||||
limit: Limit;
|
||||
counter1h: Counter;
|
||||
counter24h: Counter;
|
||||
queueLen: number;
|
||||
pendingHtlcCount: number;
|
||||
}
|
||||
|
||||
interface Limits {
|
||||
limits: NodeLimit[];
|
||||
defaultLimit: Limit;
|
||||
}
|
||||
|
||||
type ColumnId =
|
||||
| 'alias'
|
||||
| 'counter1h.success'
|
||||
| 'counter1h.fail'
|
||||
| 'counter1h.reject'
|
||||
| 'counter24h.success'
|
||||
| 'counter24h.fail'
|
||||
| 'counter24h.reject'
|
||||
| 'node'
|
||||
| 'pendingHtlcCount'
|
||||
| 'queueLen'
|
||||
| 'limit.maxPending'
|
||||
| 'limit.maxHourlyRate'
|
||||
| 'limit.mode';
|
||||
|
||||
type Order = 'asc' | 'desc';
|
||||
5
web/src/types/config.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
interface Config {}
|
||||
|
||||
interface Window {
|
||||
config: Config;
|
||||
}
|
||||
11
web/src/types/environment.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NEXT_PUBLIC_ETH_TESTNET_EXPLORER: string;
|
||||
NEXT_PUBLIC_AVAX_TESTNET_EXPLORER: string;
|
||||
NEXT_PUBLIC_CIRCLE_ATTESTATION_API: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
3
web/src/types/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
interface GetTranslationProps {
|
||||
locale: string;
|
||||
}
|
||||
9
web/src/types/services.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
interface APIError {
|
||||
code: number;
|
||||
message: string;
|
||||
details: {
|
||||
'@type': string;
|
||||
reason: string;
|
||||
domain: string;
|
||||
}[];
|
||||
}
|
||||
37
web/src/views/Home/Home.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
import { useLimits, useInfo } from 'hooks';
|
||||
import { removeLoader } from 'splashScreen';
|
||||
|
||||
import { Header, Footer, NodeTable } from './parts';
|
||||
|
||||
const Home = () => {
|
||||
const { isSuccess: isLimitsSuccess } = useLimits();
|
||||
const { isSuccess: isInfoSuccess } = useInfo();
|
||||
const { ready } = useTranslation();
|
||||
|
||||
const isSuccess = isLimitsSuccess && isInfoSuccess && ready;
|
||||
|
||||
useEffect(() => {
|
||||
removeLoader();
|
||||
}, [isSuccess]);
|
||||
|
||||
if (!isSuccess) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100dvh',
|
||||
px: { xs: 0, md: 5, xl: 10 },
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
<NodeTable />
|
||||
<Footer />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
1
web/src/views/Home/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Home';
|
||||
240
web/src/views/Home/parts/ColumnsSetting.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Drawer,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Divider,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { ClickAwayListener } from '@mui/base';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
import { SecondaryButton, Checkbox } from 'components';
|
||||
|
||||
interface ColumnsSettingProps {
|
||||
checkedSettings: { [K in ColumnId]: boolean };
|
||||
handleToggleSettings: (value: ColumnId) => void;
|
||||
}
|
||||
|
||||
interface ColumnOptionProps {
|
||||
text: string;
|
||||
isChecked: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const ColumnOption = ({ text, isChecked, onClick }: ColumnOptionProps) => (
|
||||
<ListItemButton
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={text} sx={{ fontWeight: 'bold' }} />
|
||||
<Checkbox size="small" color="primary" checked={isChecked} />
|
||||
</ListItemButton>
|
||||
);
|
||||
|
||||
const ColumnsSetting = ({
|
||||
checkedSettings,
|
||||
handleToggleSettings,
|
||||
}: ColumnsSettingProps) => {
|
||||
const { t } = useTranslation('common', { keyPrefix: 'node-table' });
|
||||
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const ColumnSettingOptions = () => (
|
||||
<ClickAwayListener onClickAway={() => setShowSettings(false)}>
|
||||
<List>
|
||||
<Typography variant="h5" sx={{ px: 3, py: 1 }}>
|
||||
{t('counter1h.title')}
|
||||
</Typography>
|
||||
<ColumnOption
|
||||
text={t('counter1h.success')}
|
||||
isChecked={checkedSettings['counter1h.success']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('counter1h.success');
|
||||
}}
|
||||
/>
|
||||
<ColumnOption
|
||||
text={t('counter1h.fail')}
|
||||
isChecked={checkedSettings['counter1h.fail']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('counter1h.fail');
|
||||
}}
|
||||
/>
|
||||
<ColumnOption
|
||||
text={t('counter1h.reject')}
|
||||
isChecked={checkedSettings['counter1h.reject']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('counter1h.reject');
|
||||
}}
|
||||
/>
|
||||
<Divider sx={{ my: 1, mx: { xs: -4, md: 0 } }} />
|
||||
<Typography variant="h5" sx={{ px: 3, py: 1 }}>
|
||||
{t('counter24h.title')}
|
||||
</Typography>
|
||||
<ColumnOption
|
||||
text={t('counter24h.success')}
|
||||
isChecked={checkedSettings['counter24h.success']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('counter24h.success');
|
||||
}}
|
||||
/>
|
||||
<ColumnOption
|
||||
text={t('counter24h.fail')}
|
||||
isChecked={checkedSettings['counter24h.fail']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('counter24h.fail');
|
||||
}}
|
||||
/>
|
||||
<ColumnOption
|
||||
text={t('counter24h.reject')}
|
||||
isChecked={checkedSettings['counter24h.reject']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('counter24h.reject');
|
||||
}}
|
||||
/>
|
||||
<Divider sx={{ my: 1, mx: { xs: -4, md: 0 } }} />
|
||||
<Typography variant="h5" sx={{ px: 3, pt: 2, pb: 1 }}>
|
||||
{t('currentFwds')}
|
||||
</Typography>
|
||||
<ColumnOption
|
||||
text={t('pendingHtlcCount')}
|
||||
isChecked={checkedSettings.pendingHtlcCount}
|
||||
onClick={() => {
|
||||
handleToggleSettings('pendingHtlcCount');
|
||||
}}
|
||||
/>
|
||||
<ColumnOption
|
||||
text={t('queueLen')}
|
||||
isChecked={checkedSettings.queueLen}
|
||||
onClick={() => {
|
||||
handleToggleSettings('queueLen');
|
||||
}}
|
||||
/>
|
||||
<Divider sx={{ my: 1, mx: { xs: -4, md: 0 } }} />
|
||||
<Typography variant="h5" sx={{ px: 3, pt: 2, pb: 1 }}>
|
||||
{t('limit.title')}
|
||||
</Typography>
|
||||
<ColumnOption
|
||||
text={t('limit.maxHourlyRate')}
|
||||
isChecked={checkedSettings['limit.maxHourlyRate']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('limit.maxHourlyRate');
|
||||
}}
|
||||
/>
|
||||
<ColumnOption
|
||||
text={t('limit.maxPending')}
|
||||
isChecked={checkedSettings['limit.maxPending']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('limit.maxPending');
|
||||
}}
|
||||
/>
|
||||
<ColumnOption
|
||||
text={t('limit.mode')}
|
||||
isChecked={checkedSettings['limit.mode']}
|
||||
onClick={() => {
|
||||
handleToggleSettings('limit.mode');
|
||||
}}
|
||||
/>
|
||||
</List>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box id="main">
|
||||
{isTablet ? (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<SecondaryButton
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
sx={{
|
||||
height: '40px',
|
||||
...(showSettings && {
|
||||
boxShadow: 'inset 0px 2px 6px rgba(0, 0, 0, 0.25)',
|
||||
}),
|
||||
'&:hover': {
|
||||
backgroundColor: 'grey.50',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ mr: 2, display: 'flex' }}>
|
||||
<Image
|
||||
src="/icons/columns.svg"
|
||||
alt="columns"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</Box>
|
||||
<Typography>{t('columns')}</Typography>
|
||||
</Box>
|
||||
</SecondaryButton>
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'block' },
|
||||
mt: 1,
|
||||
borderRadius: '8px',
|
||||
width: '208px',
|
||||
position: 'absolute',
|
||||
border: '1px solid #D3D4DB',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'grey.100',
|
||||
filter:
|
||||
'filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.12)) drop-shadow(0px 4px 12px rgba(0, 0, 0, 0.16));',
|
||||
opacity: showSettings ? 1 : 0,
|
||||
visibility: showSettings ? 'visible' : 'hidden',
|
||||
transition: 'all 150ms ease-in-out',
|
||||
}}
|
||||
>
|
||||
<ColumnSettingOptions />
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<SecondaryButton
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
sx={{
|
||||
...(showSettings && {
|
||||
boxShadow: 'inset 0px 2px 6px rgba(0, 0, 0, 0.25)',
|
||||
}),
|
||||
'&:hover': {
|
||||
backgroundColor: 'grey.50',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ mr: 0, display: 'flex' }}>
|
||||
<Image
|
||||
src="/icons/columns.svg"
|
||||
alt="columns"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</SecondaryButton>
|
||||
<Drawer
|
||||
open={showSettings}
|
||||
anchor="bottom"
|
||||
sx={{
|
||||
zIndex: 101,
|
||||
display: { xs: 'initial', md: 'none' },
|
||||
}}
|
||||
>
|
||||
<ColumnSettingOptions />
|
||||
</Drawer>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnsSetting;
|
||||
147
web/src/views/Home/parts/DefaultLimits.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import { Mode } from 'enums';
|
||||
import { useLimits } from 'hooks';
|
||||
import { Modal, PrimaryButton, EditLimitsForm } from 'components';
|
||||
import { updateDefaultLimit } from 'services/circuitbreaker';
|
||||
|
||||
interface ConfigProps {
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const Config = ({ type, value }: ConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useLimits();
|
||||
|
||||
const { defaultLimit } = data!;
|
||||
|
||||
const getString = () => {
|
||||
if (type === 'mode') {
|
||||
if (value === Mode.Block) return t(`modes.${value}`);
|
||||
if (defaultLimit.maxHourlyRate === '0' && defaultLimit.maxPending === '0')
|
||||
return '-';
|
||||
return t(`modes.${value}`);
|
||||
}
|
||||
if (defaultLimit.mode === Mode.Block) return '-';
|
||||
if (value === '0') return '∞';
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: { xs: 'auto', md: 123 },
|
||||
mr: { xs: 4, md: 8 },
|
||||
'.config-icon': { display: { xs: 'none', md: 'block' } },
|
||||
}}
|
||||
>
|
||||
<Box className="config-icon" sx={{ mr: 3, img: { display: 'block' } }}>
|
||||
<Image src={`/icons/${type}.svg`} alt={type} width={24} height={24} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography sx={{ color: 'grey.700', mb: 1 }}>{t(type)}</Typography>
|
||||
<Typography sx={{ color: 'grey.50', fontSize: '16px' }}>
|
||||
{getString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultLimits = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t: tError } = useTranslation('common', { keyPrefix: 'errors' });
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { data, refetch } = useLimits();
|
||||
|
||||
const handleModalClose = () => setIsModalOpen(false);
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: updateDefaultLimit,
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
handleModalClose();
|
||||
},
|
||||
onError: () =>
|
||||
enqueueSnackbar(tError('error-updating-default'), {
|
||||
variant: 'error',
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = (newLimit: Limit) => {
|
||||
mutate({ limit: newLimit! });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Config
|
||||
type="max-hourly-rate"
|
||||
value={data!.defaultLimit.maxHourlyRate}
|
||||
/>
|
||||
<Config type="max-pending" value={data!.defaultLimit.maxPending} />
|
||||
<Config type="mode" value={data!.defaultLimit.mode} />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<PrimaryButton onClick={() => setIsModalOpen(true)}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: 'flex',
|
||||
img: {
|
||||
width: { xs: '12px', md: '16px' },
|
||||
height: { xs: '12px', md: '16px' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/icons/edit.svg"
|
||||
alt="columns"
|
||||
width={12}
|
||||
height={12}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ color: 'inherit', display: { md: 'none' } }}
|
||||
>
|
||||
{t('edit')}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ color: 'inherit', display: { xs: 'none', md: 'block' } }}
|
||||
>
|
||||
{t('edit-defaults')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<Modal
|
||||
title={t('default-limit-modal.title') || undefined}
|
||||
open={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
>
|
||||
<EditLimitsForm
|
||||
onCancel={handleModalClose}
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={data!.defaultLimit}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultLimits;
|
||||
161
web/src/views/Home/parts/EditSelectedNodes.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import { EditLimitsForm, Modal, NodeAlias, PrimaryButton } from 'components';
|
||||
import { useLimits } from 'hooks';
|
||||
import { clearLimits, updateLimits } from 'services/circuitbreaker';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Box,
|
||||
Divider,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
interface EditSelectedNodesProps {
|
||||
selected: string[];
|
||||
}
|
||||
|
||||
const EditSelectedNodes = ({ selected }: EditSelectedNodesProps) => {
|
||||
const { t } = useTranslation('common', { keyPrefix: 'node-table' });
|
||||
const { t: tError } = useTranslation('common', { keyPrefix: 'errors' });
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { data, refetch } = useLimits();
|
||||
|
||||
const handleModalClose = () => setIsModalOpen(false);
|
||||
const isButtonDisabled = !selected.length;
|
||||
|
||||
const updateSelected = async (newLimit: Limit) => {
|
||||
const { defaultLimit } = data!;
|
||||
const isNewLimitDefault = isEqual(newLimit, defaultLimit);
|
||||
if (isNewLimitDefault) {
|
||||
await clearLimits({ nodes: selected });
|
||||
} else {
|
||||
const newLimits = selected.reduce(
|
||||
(obj, nodeId) => ({
|
||||
...obj,
|
||||
[nodeId]: newLimit,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
await updateLimits({
|
||||
limits: newLimits,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: updateSelected,
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
handleModalClose();
|
||||
},
|
||||
onError: () =>
|
||||
enqueueSnackbar(tError('error-updating-selected'), {
|
||||
variant: 'error',
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = (newLimit: Limit) => {
|
||||
mutate(newLimit);
|
||||
};
|
||||
|
||||
const getSelectedNode = () => {
|
||||
if (selected.length === 1) {
|
||||
const { limits } = data!;
|
||||
const nodeLimit = limits.find((n) => n.node === selected[0]);
|
||||
return nodeLimit?.limit;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PrimaryButton
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ mr: { xs: 0, md: 2 }, display: 'flex' }}>
|
||||
<Image
|
||||
src={
|
||||
isButtonDisabled
|
||||
? '/icons/edit-disabled.svg'
|
||||
: '/icons/edit.svg'
|
||||
}
|
||||
alt="columns"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'block' },
|
||||
color: isButtonDisabled ? '#828CC5' : 'grey.50',
|
||||
}}
|
||||
>
|
||||
{t('edit-selected')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</PrimaryButton>
|
||||
<Modal
|
||||
title={t('edit-selected-nodes') || undefined}
|
||||
open={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
>
|
||||
<EditLimitsForm
|
||||
initialValues={getSelectedNode()}
|
||||
onCancel={handleModalClose}
|
||||
onSubmit={handleSubmit}
|
||||
isNodeEdit
|
||||
/>
|
||||
<Box sx={{ mt: 6 }}>
|
||||
<Divider sx={{ mx: -6, mb: 6 }} />
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={
|
||||
<Image
|
||||
src="/icons/caret-down.svg"
|
||||
alt="expand"
|
||||
height="12"
|
||||
width="12"
|
||||
/>
|
||||
}
|
||||
aria-controls="panel1a-content"
|
||||
id="panel1a-header"
|
||||
>
|
||||
<Typography>{t('selected-peers')}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography sx={{ color: '#5C6484' }}>
|
||||
{selected.map((nodeId, index) => {
|
||||
const nodeLimit = data!.limits.find(
|
||||
({ node }) => node === nodeId
|
||||
);
|
||||
return (
|
||||
<React.Fragment key={nodeLimit!.node}>
|
||||
<NodeAlias
|
||||
alias={nodeLimit!.alias}
|
||||
nodeLimit={nodeLimit!}
|
||||
/>
|
||||
{index < selected.length - 1 ? ', ' : ''}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSelectedNodes;
|
||||
25
web/src/views/Home/parts/Footer.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
import { FOOTER_HEIGHT } from 'constant';
|
||||
|
||||
const Footer = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: FOOTER_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ textAlign: 'center', color: 'grey.700' }}>
|
||||
{t('footer')}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
74
web/src/views/Home/parts/Header.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { HEADER_HEIGHT_DESKTOP, HEADER_HEIGHT_MOBILE } from 'constant';
|
||||
import { useInfo } from 'hooks';
|
||||
|
||||
import NodeInfo from './NodeInfo';
|
||||
import DefaultLimits from './DefaultLimits';
|
||||
|
||||
const Header = () => {
|
||||
const { info } = useInfo();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: { xs: 'center', lg: 'space-between' },
|
||||
flexDirection: { xs: 'column', lg: 'row' },
|
||||
height: { xs: HEADER_HEIGHT_MOBILE, lg: HEADER_HEIGHT_DESKTOP },
|
||||
px: { xs: 4, md: 0 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: { xs: 4, lg: 0 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
mr: 4,
|
||||
img: {
|
||||
display: 'block',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/images/circuitbreaker-logo.svg"
|
||||
alt="Circuit Breaker"
|
||||
width={44}
|
||||
height={44}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="h3" sx={{ color: 'grey.50', mb: 1 }}>
|
||||
Circuit Breaker
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography sx={{ color: 'grey.50' }}>{info.version}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
mx: 2,
|
||||
backgroundColor: 'grey.700',
|
||||
height: '4px',
|
||||
width: '4px',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
<NodeInfo />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: 'grey.50',
|
||||
fontSize: '16px',
|
||||
mb: 2,
|
||||
display: { xs: 'inline-block', md: 'none' },
|
||||
}}
|
||||
>
|
||||
Default Limits
|
||||
</Typography>
|
||||
<DefaultLimits />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
73
web/src/views/Home/parts/NodeInfo.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Box, Typography, Tooltip } from '@mui/material';
|
||||
import { useInfo } from 'hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const NodeInfoTooltip = () => {
|
||||
const { t } = useTranslation();
|
||||
const { info } = useInfo();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography sx={{ color: '#5C6484', mb: 1 }}>{t('node-key')}</Typography>
|
||||
<Typography sx={{ mb: 3 }}>{info.nodeKey}</Typography>
|
||||
<Typography sx={{ color: '#5C6484', mb: 1 }}>
|
||||
{t('node-version')}
|
||||
</Typography>
|
||||
<Typography>{info.nodeVersion}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeInfo = () => {
|
||||
const { info } = useInfo();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
enterTouchDelay={0}
|
||||
placement="bottom-end"
|
||||
title={<NodeInfoTooltip />}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
path: {
|
||||
fill: '#7984AD',
|
||||
},
|
||||
'> p': {
|
||||
color: '#7984AD',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: '#5C6484',
|
||||
mr: 1,
|
||||
textDecoration: 'underline',
|
||||
textDecorationStyle: 'dashed',
|
||||
textUnderlineOffset: '2px',
|
||||
}}
|
||||
>
|
||||
{info.nodeAlias}
|
||||
</Typography>{' '}
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 0C5.61553 0 4.26215 0.410543 3.11101 1.17971C1.95987 1.94888 1.06266 3.04213 0.532846 4.32122C0.003033 5.6003 -0.13559 7.00776 0.134506 8.36563C0.404603 9.7235 1.07129 10.9708 2.05026 11.9497C3.02922 12.9287 4.2765 13.5954 5.63437 13.8655C6.99224 14.1356 8.3997 13.997 9.67879 13.4672C10.9579 12.9373 12.0511 12.0401 12.8203 10.889C13.5895 9.73785 14 8.38447 14 7C13.998 5.1441 13.2599 3.36479 11.9475 2.05247C10.6352 0.74015 8.8559 0.0020073 7 0ZM7.14583 2.91667C7.31889 2.91667 7.48807 2.96798 7.63196 3.06413C7.77585 3.16028 7.888 3.29693 7.95423 3.45682C8.02046 3.6167 8.03778 3.79264 8.00402 3.96237C7.97026 4.1321 7.88692 4.28801 7.76455 4.41038C7.64218 4.53276 7.48627 4.61609 7.31654 4.64985C7.14681 4.68362 6.97087 4.66629 6.81099 4.60006C6.6511 4.53383 6.51445 4.42168 6.4183 4.27779C6.32215 4.1339 6.27084 3.96472 6.27084 3.79167C6.27084 3.5596 6.36302 3.33704 6.52712 3.17295C6.69121 3.00885 6.91377 2.91667 7.14583 2.91667ZM8.45833 10.7917H6.125C5.97029 10.7917 5.82192 10.7302 5.71252 10.6208C5.60313 10.5114 5.54167 10.363 5.54167 10.2083C5.54167 10.0536 5.60313 9.90525 5.71252 9.79585C5.82192 9.68646 5.97029 9.625 6.125 9.625H6.5625C6.60118 9.625 6.63827 9.60963 6.66562 9.58228C6.69297 9.55494 6.70833 9.51784 6.70833 9.47917V6.85417C6.70833 6.81549 6.69297 6.7784 6.66562 6.75105C6.63827 6.7237 6.60118 6.70833 6.5625 6.70833H6.125C5.97029 6.70833 5.82192 6.64687 5.71252 6.53748C5.60313 6.42808 5.54167 6.27971 5.54167 6.125C5.54167 5.97029 5.60313 5.82192 5.71252 5.71252C5.82192 5.60312 5.97029 5.54167 6.125 5.54167H6.70833C7.01775 5.54167 7.3145 5.66458 7.53329 5.88337C7.75208 6.10217 7.875 6.39891 7.875 6.70833V9.47917C7.875 9.51784 7.89037 9.55494 7.91772 9.58228C7.94506 9.60963 7.98216 9.625 8.02083 9.625H8.45833C8.61304 9.625 8.76142 9.68646 8.87081 9.79585C8.98021 9.90525 9.04167 10.0536 9.04167 10.2083C9.04167 10.363 8.98021 10.5114 8.87081 10.6208C8.76142 10.7302 8.61304 10.7917 8.45833 10.7917Z"
|
||||
fill="#5C6484"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeInfo;
|
||||