diff --git a/.gitignore b/.gitignore index bc26871..9e00bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /circuitbreaker /webui-build -**/node_modules \ No newline at end of file +**/node_modules diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..696afca --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "prettier.configPath": "web/.prettierrc" +} diff --git a/Dockerfile b/Dockerfile index 2258071..78b4d60 100644 --- a/Dockerfile +++ b/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 diff --git a/stub.go b/stub.go index e377f1f..44550cb 100644 --- a/stub.go +++ b/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 { diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..6b8710a --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1 @@ +.git diff --git a/web/.eslintignore b/web/.eslintignore new file mode 100644 index 0000000..8c067b6 --- /dev/null +++ b/web/.eslintignore @@ -0,0 +1,8 @@ +jest.config.js +next.config.js +server.js +babel-jest.js +next-i18next.config.js + +/public +/coverage diff --git a/web/.eslintrc.js b/web/.eslintrc.js new file mode 100644 index 0000000..4aef45f --- /dev/null +++ b/web/.eslintrc.js @@ -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', + }, +}; diff --git a/webui/.gitignore b/web/.gitignore similarity index 54% rename from webui/.gitignore rename to web/.gitignore index 8996736..8925b21 100644 --- a/webui/.gitignore +++ b/web/.gitignore @@ -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 diff --git a/web/.husky/pre-commit b/web/.husky/pre-commit new file mode 100755 index 0000000..c75e779 --- /dev/null +++ b/web/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +cd web && yarn check-lint && yarn check-types diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000..fbbde30 --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,7 @@ +{ + "endOfLine": "auto", + "printWidth": 80, + "tabWidth": 2, + "trailingComma": "es5", + "singleQuote": true +} diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..f76238c --- /dev/null +++ b/web/Dockerfile @@ -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"] diff --git a/web/i18n.js b/web/i18n.js new file mode 100644 index 0000000..4d28580 --- /dev/null +++ b/web/i18n.js @@ -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; diff --git a/web/next-env.d.ts b/web/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/web/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/web/next.config.js b/web/next.config.js new file mode 100644 index 0000000..bca7a4e --- /dev/null +++ b/web/next.config.js @@ -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 []; + }, +}; diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..d8baead --- /dev/null +++ b/web/package.json @@ -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" + } +} diff --git a/web/public/fonts/HelveticaNeue.eot b/web/public/fonts/HelveticaNeue.eot new file mode 100644 index 0000000..c5a29d9 Binary files /dev/null and b/web/public/fonts/HelveticaNeue.eot differ diff --git a/web/public/fonts/HelveticaNeue.ttf b/web/public/fonts/HelveticaNeue.ttf new file mode 100644 index 0000000..794046f Binary files /dev/null and b/web/public/fonts/HelveticaNeue.ttf differ diff --git a/web/public/fonts/HelveticaNeue.woff b/web/public/fonts/HelveticaNeue.woff new file mode 100644 index 0000000..cb854d0 Binary files /dev/null and b/web/public/fonts/HelveticaNeue.woff differ diff --git a/web/public/fonts/HelveticaNeue.woff2 b/web/public/fonts/HelveticaNeue.woff2 new file mode 100644 index 0000000..53e664a Binary files /dev/null and b/web/public/fonts/HelveticaNeue.woff2 differ diff --git a/web/public/fonts/HelveticaNeueBold.eot b/web/public/fonts/HelveticaNeueBold.eot new file mode 100644 index 0000000..3783533 Binary files /dev/null and b/web/public/fonts/HelveticaNeueBold.eot differ diff --git a/web/public/fonts/HelveticaNeueBold.ttf b/web/public/fonts/HelveticaNeueBold.ttf new file mode 100644 index 0000000..9aa746a Binary files /dev/null and b/web/public/fonts/HelveticaNeueBold.ttf differ diff --git a/web/public/fonts/HelveticaNeueBold.woff2 b/web/public/fonts/HelveticaNeueBold.woff2 new file mode 100644 index 0000000..2be49dc Binary files /dev/null and b/web/public/fonts/HelveticaNeueBold.woff2 differ diff --git a/web/public/icons/caret-down.svg b/web/public/icons/caret-down.svg new file mode 100644 index 0000000..d6686da --- /dev/null +++ b/web/public/icons/caret-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/check-primary.svg b/web/public/icons/check-primary.svg new file mode 100644 index 0000000..da683a9 --- /dev/null +++ b/web/public/icons/check-primary.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/check.svg b/web/public/icons/check.svg new file mode 100644 index 0000000..8e4c3b3 --- /dev/null +++ b/web/public/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/close.svg b/web/public/icons/close.svg new file mode 100644 index 0000000..9fc1f49 --- /dev/null +++ b/web/public/icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/columns.svg b/web/public/icons/columns.svg new file mode 100644 index 0000000..56491e8 --- /dev/null +++ b/web/public/icons/columns.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/edit-disabled.svg b/web/public/icons/edit-disabled.svg new file mode 100644 index 0000000..6b227df --- /dev/null +++ b/web/public/icons/edit-disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/edit.svg b/web/public/icons/edit.svg new file mode 100644 index 0000000..6fdc3f1 --- /dev/null +++ b/web/public/icons/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/exclamation.svg b/web/public/icons/exclamation.svg new file mode 100644 index 0000000..1f8a6d7 --- /dev/null +++ b/web/public/icons/exclamation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/icons/info.svg b/web/public/icons/info.svg new file mode 100644 index 0000000..49b3147 --- /dev/null +++ b/web/public/icons/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/max-hourly-rate.svg b/web/public/icons/max-hourly-rate.svg new file mode 100644 index 0000000..898153e --- /dev/null +++ b/web/public/icons/max-hourly-rate.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/max-pending.svg b/web/public/icons/max-pending.svg new file mode 100644 index 0000000..ed49e15 --- /dev/null +++ b/web/public/icons/max-pending.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/minus.svg b/web/public/icons/minus.svg new file mode 100644 index 0000000..67c3165 --- /dev/null +++ b/web/public/icons/minus.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/web/public/icons/mode.svg b/web/public/icons/mode.svg new file mode 100644 index 0000000..3a9d1c8 --- /dev/null +++ b/web/public/icons/mode.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/icons/search-icon.svg b/web/public/icons/search-icon.svg new file mode 100644 index 0000000..e2f7feb --- /dev/null +++ b/web/public/icons/search-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/sort-asc.svg b/web/public/icons/sort-asc.svg new file mode 100644 index 0000000..f379c07 --- /dev/null +++ b/web/public/icons/sort-asc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/icons/sort-desc.svg b/web/public/icons/sort-desc.svg new file mode 100644 index 0000000..1f08a61 --- /dev/null +++ b/web/public/icons/sort-desc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/icons/sort.svg b/web/public/icons/sort.svg new file mode 100644 index 0000000..91b87ac --- /dev/null +++ b/web/public/icons/sort.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/images/circuitbreaker-logo.svg b/web/public/images/circuitbreaker-logo.svg new file mode 100644 index 0000000..7a659fc --- /dev/null +++ b/web/public/images/circuitbreaker-logo.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/images/favicon.ico b/web/public/images/favicon.ico new file mode 100644 index 0000000..b34fd05 Binary files /dev/null and b/web/public/images/favicon.ico differ diff --git a/webui/public/index.html b/web/public/index.html similarity index 100% rename from webui/public/index.html rename to web/public/index.html diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json new file mode 100644 index 0000000..268ced4 --- /dev/null +++ b/web/public/locales/en/common.json @@ -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" + } + } +} diff --git a/web/src/components/Checkbox/Checkbox.tsx b/web/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..6b7cfa9 --- /dev/null +++ b/web/src/components/Checkbox/Checkbox.tsx @@ -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')(({ 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 {} + +// Inspired by blueprintjs +const Checkbox = ({ variant = 'primary', ...rest }: CheckboxProps) => ( + } + icon={} + indeterminateIcon={} + inputProps={{ 'aria-label': 'Checkbox' }} + sx={{ + '&:hover': { bgcolor: 'transparent' }, + }} + {...rest} + /> +); + +export default Checkbox; diff --git a/web/src/components/Checkbox/index.ts b/web/src/components/Checkbox/index.ts new file mode 100644 index 0000000..dd6385d --- /dev/null +++ b/web/src/components/Checkbox/index.ts @@ -0,0 +1 @@ +export { default } from './Checkbox'; diff --git a/web/src/components/EditLimitsForm/EditLimitsForm.tsx b/web/src/components/EditLimitsForm/EditLimitsForm.tsx new file mode 100644 index 0000000..7b9566c --- /dev/null +++ b/web/src/components/EditLimitsForm/EditLimitsForm.tsx @@ -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(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 ( + + {isNodeEdit && ( + + handleUseDefaultValuesChecked(e.target.checked)} + sx={{ + mr: 2, + 'input:hover ~ span': { + boxShadow: 'none', + }, + }} + /> + {t('use-default-values')} + + )} + + + + {t('max-hourly-rate')} + { + const { + target: { value }, + } = e; + if (value === '0') setIsMaxHourlyRateUnlimited(true); + setMaxHourlyRate(value); + }} + disabled={ + isMaxHourlyRateUnlimited || isUseDefaultValues || isModeBlock + } + /> + + + + { + setIsMaxHourlyRateUnlimited(e.target.checked); + if (e.target.checked) setMaxHourlyRate('0'); + else setMaxHourlyRate(''); + }} + disabled={isUseDefaultValues || isModeBlock} + sx={{ mr: '6px' }} + /> + {t('unlimited')} + + + + + + {t('max-pending')} + { + const { + target: { value }, + } = e; + setMaxPending(value); + if (value === '0') setIsMaxPendingUnlimited(true); + }} + disabled={ + isMaxPendingUnlimited || isUseDefaultValues || isModeBlock + } + /> + + + + { + setIsMaxPendingUnlimited(e.target.checked); + if (e.target.checked) setMaxPending('0'); + else setMaxPending(''); + }} + disabled={isUseDefaultValues || isModeBlock} + sx={{ mr: '6px' }} + /> + {t('unlimited')} + + + + + + {t('mode')} + + {isModeBlock && ( + + + info + + + {t('blocked-description')} + + + )} + {isModeQueue && ( + + {t('queue-alert')} + + )} + + + + {t('save')} + + + {t('cancel')} + + + + ); +}; + +export default EditLimitsForm; diff --git a/web/src/components/EditLimitsForm/index.ts b/web/src/components/EditLimitsForm/index.ts new file mode 100644 index 0000000..deec5fc --- /dev/null +++ b/web/src/components/EditLimitsForm/index.ts @@ -0,0 +1 @@ +export { default } from './EditLimitsForm'; diff --git a/web/src/components/Fonts/Fonts.tsx b/web/src/components/Fonts/Fonts.tsx new file mode 100644 index 0000000..a3d813f --- /dev/null +++ b/web/src/components/Fonts/Fonts.tsx @@ -0,0 +1,32 @@ +import { css, Global } from '@emotion/react'; + +export interface FontsProps { + fontDisplay?: 'auto' | 'block' | 'fallback' | 'optional' | 'swap'; +} + +const Fonts = ({ fontDisplay = 'auto' }: FontsProps) => ( + +); + +export default Fonts; diff --git a/web/src/components/Fonts/index.ts b/web/src/components/Fonts/index.ts new file mode 100644 index 0000000..9667158 --- /dev/null +++ b/web/src/components/Fonts/index.ts @@ -0,0 +1 @@ +export { default } from './Fonts'; diff --git a/web/src/components/Modal/Modal.tsx b/web/src/components/Modal/Modal.tsx new file mode 100644 index 0000000..412af1c --- /dev/null +++ b/web/src/components/Modal/Modal.tsx @@ -0,0 +1,26 @@ +import { Dialog, DialogTitle, DialogProps, Drawer } from '@mui/material'; + +const Modal = ({ + title, + children, + ...rest +}: React.PropsWithChildren) => ( + <> + + {title} + {children} + + + {title} + {children} + + +); + +export default Modal; diff --git a/web/src/components/Modal/index.ts b/web/src/components/Modal/index.ts new file mode 100644 index 0000000..0690fec --- /dev/null +++ b/web/src/components/Modal/index.ts @@ -0,0 +1 @@ +export { default } from './Modal'; diff --git a/web/src/components/NodeAlias/NodeAlias.tsx b/web/src/components/NodeAlias/NodeAlias.tsx new file mode 100644 index 0000000..0b9ca00 --- /dev/null +++ b/web/src/components/NodeAlias/NodeAlias.tsx @@ -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; diff --git a/web/src/components/NodeAlias/index.ts b/web/src/components/NodeAlias/index.ts new file mode 100644 index 0000000..da3b07b --- /dev/null +++ b/web/src/components/NodeAlias/index.ts @@ -0,0 +1 @@ +export { default } from './NodeAlias'; diff --git a/web/src/components/PrimaryButton/PrimaryButton.tsx b/web/src/components/PrimaryButton/PrimaryButton.tsx new file mode 100644 index 0000000..c2c50b0 --- /dev/null +++ b/web/src/components/PrimaryButton/PrimaryButton.tsx @@ -0,0 +1,18 @@ +import { Button, ButtonProps } from '@mui/material'; + +const PrimaryButton = ({ sx, ...rest }: ButtonProps) => ( +