Web UI improvements

This commit is contained in:
Onur Fesci 2023-03-22 17:28:11 +03:00
parent 97297aa6f5
commit b7945f57a7
123 changed files with 6056 additions and 18029 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
/circuitbreaker
/webui-build
**/node_modules
**/node_modules

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"prettier.configPath": "web/.prettierrc"
}

View file

@ -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
View file

@ -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
View file

@ -0,0 +1 @@
.git

8
web/.eslintignore Normal file
View 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
View 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',
},
};

View file

@ -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
View 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
View file

@ -0,0 +1,7 @@
{
"endOfLine": "auto",
"printWidth": 80,
"tabWidth": 2,
"trailingComma": "es5",
"singleQuote": true
}

45
web/Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View 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"
}
}
}

View 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;

View file

@ -0,0 +1 @@
export { default } from './Checkbox';

View 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;

View file

@ -0,0 +1 @@
export { default } from './EditLimitsForm';

View 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;

View file

@ -0,0 +1 @@
export { default } from './Fonts';

View 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;

View file

@ -0,0 +1 @@
export { default } from './Modal';

View 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;

View file

@ -0,0 +1 @@
export { default } from './NodeAlias';

View 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;

View file

@ -0,0 +1 @@
export { default } from './PrimaryButton';

View 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;

View file

@ -0,0 +1 @@
export { default } from './SecondaryButton';

View 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;

View file

@ -0,0 +1 @@
export { default as ErrorSnackbar } from './ErrorSnackbar';

View 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
View 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
View file

@ -0,0 +1 @@
export { getConfig } from './config';

View file

@ -0,0 +1 @@
export * from './style';

View file

@ -0,0 +1,3 @@
export const HEADER_HEIGHT_DESKTOP = '80px';
export const HEADER_HEIGHT_MOBILE = '158px';
export const FOOTER_HEIGHT = '63px';

View 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
View file

@ -0,0 +1,2 @@
export * from './circuitbreaker';
export * from './routes';

3
web/src/enums/routes.ts Normal file
View file

@ -0,0 +1,3 @@
export enum Route {
Home = '/',
}

17
web/src/global.css Normal file
View 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
View 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
View 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;

View 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
View 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;

View 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
View file

@ -0,0 +1,5 @@
import { Home } from 'views';
const HomePage = () => <Home />;
export default HomePage;

View file

@ -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;

View file

@ -0,0 +1 @@
export { default } from './QueryClientProvider';

View file

@ -0,0 +1 @@
export { default as QueryClientProvider } from './QueryClientProvider';

View 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;

View file

@ -0,0 +1,2 @@
export { default as loaderStyles } from './loaderStyles';
export { default as removeLoader } from './removeLoader';

View 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;

View 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;

View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
const spacing = 4;
export default spacing;

View 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
View 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
View file

@ -0,0 +1,5 @@
interface Config {}
interface Window {
config: Config;
}

11
web/src/types/environment.d.ts vendored Normal file
View 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
View file

@ -0,0 +1,3 @@
interface GetTranslationProps {
locale: string;
}

9
web/src/types/services.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
interface APIError {
code: number;
message: string;
details: {
'@type': string;
reason: string;
domain: string;
}[];
}

View 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;

View file

@ -0,0 +1 @@
export { default } from './Home';

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

Some files were not shown because too many files have changed in this diff Show more