diff --git a/deploy-web/package-lock.json b/deploy-web/package-lock.json index 446004a50..d438e3ab2 100644 --- a/deploy-web/package-lock.json +++ b/deploy-web/package-lock.json @@ -83,7 +83,6 @@ "material-ui-popup-state": "^4.0.2", "nanoid": "^3.3.4", "next": "^14.1.0", - "next-dark-mode": "^3.0.0", "next-nprogress-bar": "^2.1.2", "next-pwa": "^5.6.0", "next-qrcode": "^2.1.0", @@ -492,11 +491,6 @@ "ajv": ">=8" } }, - "node_modules/@assortment/darkmodejs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@assortment/darkmodejs/-/darkmodejs-1.2.1.tgz", - "integrity": "sha512-kP8ZJKG4BluaYSAwsM3u6s4Zoa2NjUaQbKDBefhpYTyWuGxc+OeNvndsVZz4CIvCcUAMXbu9FX9HeKptfoMkOg==" - }, "node_modules/@auth0/nextjs-auth0": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@auth0/nextjs-auth0/-/nextjs-auth0-3.1.0.tgz", @@ -15567,14 +15561,6 @@ "safe-buffer": "~5.1.1" } }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cookie-es": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.0.0.tgz", @@ -21188,20 +21174,6 @@ } } }, - "node_modules/next-dark-mode": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/next-dark-mode/-/next-dark-mode-3.0.0.tgz", - "integrity": "sha512-DNiKexCzNmzPuUZ9Udumo1ZOcZ7grxg1PGm72vBmjfwSW6P4Jm2WjUqaM74jb37NRflU3PjJxhmXTuaa4dFiTQ==", - "dependencies": { - "@assortment/darkmodejs": "^1.2.1", - "nookies": "^2.5.2" - }, - "peerDependencies": { - "next": ">=9.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/next-nprogress-bar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/next-nprogress-bar/-/next-nprogress-bar-2.1.2.tgz", @@ -21382,15 +21354,6 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, - "node_modules/nookies": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/nookies/-/nookies-2.5.2.tgz", - "integrity": "sha512-x0TRSaosAEonNKyCrShoUaJ5rrT5KHRNZ5DwPCuizjgrnkpE5DRf3VL7AyyQin4htict92X1EQ7ejDbaHDVdYA==", - "dependencies": { - "cookie": "^0.4.1", - "set-cookie-parser": "^2.4.6" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -23875,11 +23838,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "node_modules/set-cookie-parser": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", - "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==" - }, "node_modules/sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", diff --git a/deploy-web/package.json b/deploy-web/package.json index 9be830238..91c7c300c 100644 --- a/deploy-web/package.json +++ b/deploy-web/package.json @@ -88,7 +88,6 @@ "material-ui-popup-state": "^4.0.2", "nanoid": "^3.3.4", "next": "^14.1.0", - "next-dark-mode": "^3.0.0", "next-nprogress-bar": "^2.1.2", "next-pwa": "^5.6.0", "next-qrcode": "^2.1.0", diff --git a/deploy-web/src/app/settings/CertificateDisplay.tsx b/deploy-web/src/app/settings/CertificateDisplay.tsx new file mode 100644 index 000000000..7151db11d --- /dev/null +++ b/deploy-web/src/app/settings/CertificateDisplay.tsx @@ -0,0 +1,118 @@ +"use client"; +import { useState } from "react"; +import { useCertificate } from "../../context/CertificateProvider"; +import { ExportCertificate } from "./ExportCertificate"; +import { useWallet } from "@src/context/WalletProvider"; +import { BinMinusIn, Check, MoreHoriz, PlusCircle, Refresh, WarningTriangle } from "iconoir-react"; +import { MdAutorenew, MdGetApp } from "react-icons/md"; +import { FormPaper } from "@src/components/sdl/FormPaper"; +import { CustomTooltip } from "@src/components/shared/CustomTooltip"; +import { Button } from "@src/components/ui/button"; +import Spinner from "@src/components/shared/Spinner"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@src/components/ui/dropdown-menu"; +import { CustomDropdownLinkItem } from "@src/components/shared/CustomDropdownLinkItem"; + +export function CertificateDisplay() { + const [isExportingCert, setIsExportingCert] = useState(false); + const { + selectedCertificate, + isLocalCertMatching, + isLoadingCertificates, + loadValidCertificates, + localCert, + createCertificate, + isCreatingCert, + regenerateCertificate, + revokeCertificate + } = useCertificate(); + const { address } = useWallet(); + + const onRegenerateCert = () => { + regenerateCertificate(); + }; + + const onRevokeCert = () => { + if (selectedCertificate) revokeCertificate(selectedCertificate); + }; + + return ( + <> + {address && ( + +
+

+ {selectedCertificate ? ( + + Current certificate:{" "} +

+ {selectedCertificate.serial} +
+ + ) : ( + "No local certificate." + )} +

+ + {selectedCertificate && !isLocalCertMatching && ( + + + + )} +
+ + {!selectedCertificate && ( +
+ +
+ )} + + + + {selectedCertificate && ( +
+ + + + + + {/** If local, regenerate else create */} + {selectedCertificate.parsed === localCert?.certPem ? ( + onRegenerateCert()} icon={}> + Regenerate + + ) : ( + createCertificate()} icon={}> + Create + + )} + + onRevokeCert()} icon={}> + Revoke + + setIsExportingCert(true)} icon={}> + Export + + + +
+ )} +
+ )} + + {isExportingCert && setIsExportingCert(false)} />} + + ); +} diff --git a/deploy-web/src/components/certificates/CertificateList.tsx b/deploy-web/src/app/settings/CertificateList.tsx similarity index 58% rename from deploy-web/src/components/certificates/CertificateList.tsx rename to deploy-web/src/app/settings/CertificateList.tsx index 9761cda57..51761800d 100644 --- a/deploy-web/src/components/certificates/CertificateList.tsx +++ b/deploy-web/src/app/settings/CertificateList.tsx @@ -1,11 +1,12 @@ -import { useCertificate } from "../../context/CertificateProvider"; -import CheckIcon from "@mui/icons-material/Check"; +"use client"; import { FormattedDate } from "react-intl"; -import { Box, Button, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; import { CertificateDisplay } from "./CertificateDisplay"; -import { CustomTableHeader, CustomTableRow } from "../shared/CustomTable"; import { useWallet } from "@src/context/WalletProvider"; -import { ConnectWallet } from "../shared/ConnectWallet"; +import { Check } from "iconoir-react"; +import { useCertificate } from "@src/context/CertificateProvider"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@src/components/ui/table"; +import { ConnectWallet } from "@src/components/shared/ConnectWallet"; +import { Button } from "@src/components/ui/button"; type Props = {}; @@ -14,36 +15,36 @@ export const CertificateList: React.FunctionComponent = ({}) => { const { address } = useWallet(); return ( - +
{address ? ( - - - +
+
+ - Selected - Local cert - Issued on - Expires - Serial - + Selected + Local cert + Issued on + Expires + Serial + {validCertificates?.length > 0 && ( - )} - + - + {validCertificates.map(cert => { const isCurrentCert = cert.serial === selectedCertificate?.serial; return ( - - {isCurrentCert && } - {cert.parsed === localCert?.certPem && } + + {isCurrentCert && } + {cert.parsed === localCert?.certPem && } @@ -52,34 +53,33 @@ export const CertificateList: React.FunctionComponent = ({}) => { - {cert.serial} +

{cert.serial}

-
+ ); })}
{!isLoadingCertificates && validCertificates.length === 0 && ( - - No certificates. - +
+

No certificates.

+
)} -
+
) : ( )} -
+ ); }; - diff --git a/deploy-web/src/app/settings/ColorModeSelect.tsx b/deploy-web/src/app/settings/ColorModeSelect.tsx new file mode 100644 index 000000000..39fc1a53d --- /dev/null +++ b/deploy-web/src/app/settings/ColorModeSelect.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; +import { FormItem } from "@src/components/ui/form"; +import { Label } from "@src/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui/select"; + +type Props = {}; + +export const ColorModeSelect: React.FunctionComponent = () => { + const { setTheme, theme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + const onThemeClick = (theme: string) => { + setTheme(theme); + document.cookie = `theme=${theme}; path=/`; + }; + + return ( + + + + + ); +}; diff --git a/deploy-web/src/app/settings/ExportCertificate.tsx b/deploy-web/src/app/settings/ExportCertificate.tsx new file mode 100644 index 000000000..1acf80658 --- /dev/null +++ b/deploy-web/src/app/settings/ExportCertificate.tsx @@ -0,0 +1,60 @@ +"use client"; +import { useEffect } from "react"; +import { useSelectedWalletFromStorage } from "@src/utils/walletUtils"; +import { event } from "nextjs-google-analytics"; +import { AnalyticsEvents } from "@src/utils/analytics"; +import { Popup } from "@src/components/shared/Popup"; +import { Alert } from "@src/components/ui/alert"; +import { CodeSnippet } from "@src/components/shared/CodeSnippet"; + +export function ExportCertificate({ isOpen, onClose }: React.PropsWithChildren<{ isOpen: boolean; onClose: () => void }>) { + const selectedWallet = useSelectedWalletFromStorage(); + + useEffect(() => { + async function init() { + event(AnalyticsEvents.EXPORT_CERTIFICATE, { + category: "certificates", + label: "Export certificate" + }); + } + + init(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {selectedWallet && selectedWallet.cert && selectedWallet.certKey ? ( +
+

Cert

+
+ +
+

Key

+ +
+ ) : ( + + Unable to find local certificate. Meaning you have a certificate on chain but not in the tool. We suggest you regenerate a new one to be able to use + the tool properly. + + )} +
+ ); +} diff --git a/deploy-web/src/app/settings/SelectNetworkModal.tsx b/deploy-web/src/app/settings/SelectNetworkModal.tsx new file mode 100644 index 000000000..d7f3bd8c3 --- /dev/null +++ b/deploy-web/src/app/settings/SelectNetworkModal.tsx @@ -0,0 +1,101 @@ +"use client"; +import { mainnetId } from "@src/utils/constants"; +import { useState } from "react"; +import { networks } from "@src/store/networkStore"; +import { cn } from "@src/utils/styleUtils"; +import { useSettings } from "@src/context/SettingsProvider"; +import { Popup } from "@src/components/shared/Popup"; +import { RadioGroup, RadioGroupItem } from "@src/components/ui/radio-group"; +import { buttonVariants } from "@src/components/ui/button"; +import { Badge } from "@src/components/ui/badge"; +import { Alert, AlertDescription, AlertTitle } from "@src/components/ui/alert"; + +export const SelectNetworkModal = ({ onClose }) => { + const { selectedNetworkId } = useSettings(); + const [localSelectedNetworkId, setLocalSelectedNetworkId] = useState(selectedNetworkId); + + const handleSelectNetwork = network => { + setLocalSelectedNetworkId(network.id); + }; + + const handleSaveChanges = () => { + if (selectedNetworkId !== localSelectedNetworkId) { + // Set in the settings and local storage + localStorage.setItem("selectedNetworkId", localSelectedNetworkId); + // Reset the ui to reload the settings for the currently selected network + + location.reload(); + } else { + onClose(); + } + }; + + return ( + + +
    + {networks.map(network => { + return ( +
  • handleSelectNetwork(network)} + className={cn( + buttonVariants({ variant: "text" }), + { ["pointer-events-none text-muted-foreground"]: !network.enabled }, + "flex h-auto cursor-pointer items-center justify-start" + )} + > +
    + +
    +
    +
    + + {network.title} + {" - "} + {network.version} + + {network.id !== mainnetId && ( + Experimental + )} +
    +
    {network.description}
    +
    +
  • + ); + })} +
+
+ + {localSelectedNetworkId !== mainnetId && ( + + Warning + + Some features are experimental and may not work as intented on the testnet or sandbox. + + )} +
+ ); +}; diff --git a/deploy-web/src/app/settings/SettingsContainer.tsx b/deploy-web/src/app/settings/SettingsContainer.tsx new file mode 100644 index 000000000..ea0185787 --- /dev/null +++ b/deploy-web/src/app/settings/SettingsContainer.tsx @@ -0,0 +1,57 @@ +"use client"; +import { SettingsForm } from "@src/app/settings/SettingsForm"; +import { ColorModeSelect } from "@src/app/settings/ColorModeSelect"; +import { PageContainer } from "@src/components/shared/PageContainer"; +import { SettingsLayout, SettingsTabs } from "@src/app/settings/SettingsLayout"; +import { Fieldset } from "@src/components/shared/Fieldset"; +import { useState } from "react"; +import { useSelectedNetwork } from "@src/hooks/useSelectedNetwork"; +import { LabelValue } from "@src/components/shared/LabelValue"; +import { Button } from "@src/components/ui/button"; +import { Edit } from "iconoir-react"; +import { SelectNetworkModal } from "@src/app/settings/SelectNetworkModal"; +import { CertificateList } from "./CertificateList"; + +type Props = {}; + +export const SettingsContainer: React.FunctionComponent = ({}) => { + const [isSelectingNetwork, setIsSelectingNetwork] = useState(false); + const selectedNetwork = useSelectedNetwork(); + + const onSelectNetworkModalClose = () => { + setIsSelectingNetwork(false); + }; + + return ( + + + {isSelectingNetwork && } +
+
+ + {selectedNetwork?.title} + + +
+ } + /> + + + + +
+ +
+ + +
+ +
+
+
+ ); +}; diff --git a/deploy-web/src/components/settings/SettingsForm.tsx b/deploy-web/src/app/settings/SettingsForm.tsx similarity index 55% rename from deploy-web/src/components/settings/SettingsForm.tsx rename to deploy-web/src/app/settings/SettingsForm.tsx index 25942d451..51ba6dd0b 100644 --- a/deploy-web/src/components/settings/SettingsForm.tsx +++ b/deploy-web/src/app/settings/SettingsForm.tsx @@ -1,70 +1,27 @@ +"use client"; import { useState, useRef } from "react"; -import { - Box, - Typography, - Button, - FormLabel, - TextField, - FormControlLabel, - FormControl, - Switch, - FormGroup, - InputAdornment, - IconButton, - CircularProgress, - ClickAwayListener, - Autocomplete -} from "@mui/material"; -import RefreshIcon from "@mui/icons-material/Refresh"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import { useSettings } from "../../context/SettingsProvider"; import { Controller, useForm } from "react-hook-form"; -import { makeStyles } from "tss-react/mui"; import { NodeStatus } from "@src/components/shared/NodeStatus"; import { isUrl } from "@src/utils/stringUtils"; -import { cx } from "@emotion/css"; +import { BlockchainNode, useSettings } from "@src/context/SettingsProvider/SettingsProviderContext"; +import { SwitchWithLabel } from "@src/components/ui/switch"; +import { Label } from "@src/components/ui/label"; +import FormControl from "@mui/material/FormControl"; +import { Button } from "@src/components/ui/button"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Autocomplete from "@mui/material/Autocomplete"; +import TextField from "@mui/material/TextField"; +import FormGroup from "@mui/material/FormGroup"; +import Spinner from "@src/components/shared/Spinner"; +import { NavArrowDown, Refresh } from "iconoir-react"; +import { cn } from "@src/utils/styleUtils"; +import InputAdornment from "@mui/material/InputAdornment"; type Props = {}; -const useStyles = makeStyles()(theme => ({ - title: { - fontSize: "1.5rem", - fontWeight: "bold" - }, - form: { - padding: "1rem 0 0" - }, - fieldRow: { - display: "flex", - alignItems: "center", - marginBottom: ".5rem" - }, - formLabel: { - flexBasis: "20%", - minWidth: 150, - paddingRight: "1rem" - }, - formControl: { - width: "100%" - }, - formValue: { - flexGrow: 1 - }, - submitButton: { - marginLeft: "1rem" - }, - nodeInput: { - paddingRight: "1rem !important" - }, - inputClickable: { - cursor: "pointer" - } -})); - export const SettingsForm: React.FunctionComponent = ({}) => { const [isEditing, setIsEditing] = useState(false); const [isNodesOpen, setIsNodesOpen] = useState(false); - const { classes } = useStyles(); const { settings, setSettings, refreshNodeStatuses, isRefreshingNodeStatus } = useSettings(); const { handleSubmit, @@ -72,27 +29,26 @@ export const SettingsForm: React.FunctionComponent = ({}) => { reset, formState: { errors } } = useForm(); - const formRef = useRef(null); + const formRef = useRef(null); const { selectedNode, nodes } = settings; - const onIsCustomNodeChange = event => { - const isChecked = event.target.checked; - const apiEndpoint = isChecked ? settings.apiEndpoint : selectedNode.api; - const rpcEndpoint = isChecked ? settings.rpcEndpoint : selectedNode.rpc; + const onIsCustomNodeChange = (checked: boolean) => { + const apiEndpoint = checked ? settings.apiEndpoint : (selectedNode?.api as string); + const rpcEndpoint = checked ? settings.rpcEndpoint : (selectedNode?.rpc as string); reset(); - const newSettings = { ...settings, isCustomNode: isChecked, apiEndpoint, rpcEndpoint }; + const newSettings = { ...settings, isCustomNode: checked, apiEndpoint, rpcEndpoint }; setSettings(newSettings); refreshNodeStatuses(newSettings); }; const onNodeChange = (event, newNodeId) => { const newNode = nodes.find(n => n.id === newNodeId); - const apiEndpoint = newNode.api; - const rpcEndpoint = newNode.rpc; + const apiEndpoint = newNode?.api as string; + const rpcEndpoint = newNode?.rpc as string; - setSettings({ ...settings, apiEndpoint, rpcEndpoint, selectedNode: newNode }); + setSettings({ ...settings, apiEndpoint, rpcEndpoint, selectedNode: newNode as BlockchainNode }); }; const onRefreshNodeStatus = async () => { @@ -113,21 +69,18 @@ export const SettingsForm: React.FunctionComponent = ({}) => { }; return ( - - } - label="Custom node" - labelPlacement="start" - sx={{ marginLeft: 0 }} - /> +
+
+ +
{settings.isCustomNode && ( -
-
- Api Endpoint: + +
+ {isEditing ? ( - + = ({}) => { variant="outlined" error={!!fieldState.error} helperText={fieldState.error && helperText} - className={classes.formValue} + className="flex-1" size="small" /> ); @@ -154,17 +107,15 @@ export const SettingsForm: React.FunctionComponent = ({}) => { /> ) : ( - - {settings.apiEndpoint} - +

{settings.apiEndpoint}

)}
-
- Rpc Endpoint: +
+ {isEditing ? ( - + = ({}) => { variant="outlined" error={!!fieldState.error} helperText={fieldState.error && helperText} - className={classes.formValue} + className="flex-1" size="small" /> ); @@ -191,15 +142,13 @@ export const SettingsForm: React.FunctionComponent = ({}) => { /> ) : ( - - {settings.rpcEndpoint} - +

{settings.rpcEndpoint}

)}
- +
{!isEditing && ( - )} @@ -209,41 +158,35 @@ export const SettingsForm: React.FunctionComponent = ({}) => { - )} - +
)} {!settings.isCustomNode && ( - +
- - +
+ n.id)} - value={settings.selectedNode.id} - defaultValue={settings.selectedNode.id} + value={settings.selectedNode?.id} + defaultValue={settings.selectedNode?.id} fullWidth + size="small" onChange={onNodeChange} renderInput={params => ( setIsNodesOpen(false)}> @@ -254,13 +197,13 @@ export const SettingsForm: React.FunctionComponent = ({}) => { onClick={() => setIsNodesOpen(prev => !prev)} InputProps={{ ...params.InputProps, - classes: { root: cx(classes.nodeInput, classes.inputClickable), input: classes.inputClickable }, + classes: { root: cn("!pr-3 cursor-pointer"), input: "cursor-pointer" }, endAdornment: ( - - - - +
+ +
+
) }} @@ -271,29 +214,25 @@ export const SettingsForm: React.FunctionComponent = ({}) => { const node = nodes.find(n => n.id === option); return ( - +
  • {option}
    - - + +
  • ); }} disabled={settings.isCustomNode} />
    - - onRefreshNodeStatus()} aria-label="refresh" disabled={isRefreshingNodeStatus}> - {isRefreshingNodeStatus ? : } - - - +
    + +
    +
    - +
    )} -
    +
    ); }; diff --git a/deploy-web/src/app/settings/SettingsLayout.tsx b/deploy-web/src/app/settings/SettingsLayout.tsx new file mode 100644 index 000000000..38ab79b6b --- /dev/null +++ b/deploy-web/src/app/settings/SettingsLayout.tsx @@ -0,0 +1,56 @@ +"use client"; +import React, { ReactNode } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { UrlService } from "@src/utils/urlUtils"; +import { useRouter } from "next/navigation"; +import { ErrorFallback } from "@src/components/shared/ErrorFallback"; +import { Tabs, TabsList, TabsTrigger } from "@src/components/ui/tabs"; +import { cn } from "@src/utils/styleUtils"; + +export enum SettingsTabs { + GENERAL = "GENERAL", + AUTHORIZATIONS = "AUTHORIZATIONS" +} + +type Props = { + page: SettingsTabs; + children?: ReactNode; + title: string; + headerActions?: ReactNode; +}; + +export const SettingsLayout: React.FunctionComponent = ({ children, page, title, headerActions }) => { + const router = useRouter(); + + const handleTabChange = (newValue: SettingsTabs) => { + switch (newValue) { + case SettingsTabs.AUTHORIZATIONS: + router.push(UrlService.settingsAuthorizations()); + break; + case SettingsTabs.GENERAL: + default: + router.push(UrlService.settings()); + break; + } + }; + + return ( + + + + General + + + Authorizations + + + +
    +

    {title}

    + {headerActions} +
    + + {children} +
    + ); +}; diff --git a/deploy-web/src/components/settings/AllowanceGrantedRow.tsx b/deploy-web/src/app/settings/authorizations/AllowanceGrantedRow.tsx similarity index 73% rename from deploy-web/src/components/settings/AllowanceGrantedRow.tsx rename to deploy-web/src/app/settings/authorizations/AllowanceGrantedRow.tsx index 6e0444e3f..4b44df46a 100644 --- a/deploy-web/src/components/settings/AllowanceGrantedRow.tsx +++ b/deploy-web/src/app/settings/authorizations/AllowanceGrantedRow.tsx @@ -1,12 +1,12 @@ +"use client"; import React, { ReactNode } from "react"; -import { TableCell } from "@mui/material"; -import { CustomTableRow } from "../shared/CustomTable"; -import { Address } from "../shared/Address"; import { FormattedTime } from "react-intl"; import { AllowanceType } from "@src/types/grant"; -import { AKTAmount } from "../shared/AKTAmount"; import { coinToUDenom } from "@src/utils/priceUtils"; import { getAllowanceTitleByType } from "@src/utils/grants"; +import { TableCell, TableRow } from "@src/components/ui/table"; +import { Address } from "@src/components/shared/Address"; +import { AKTAmount } from "@src/components/shared/AKTAmount"; type Props = { allowance: AllowanceType; @@ -14,10 +14,8 @@ type Props = { }; export const AllowanceGrantedRow: React.FunctionComponent = ({ allowance }) => { - // const denomData = useDenomData(grant.authorization.spend_limit.denom); - return ( - + {getAllowanceTitleByType(allowance)}
    @@ -28,6 +26,6 @@ export const AllowanceGrantedRow: React.FunctionComponent = ({ allowance - + ); }; diff --git a/deploy-web/src/components/settings/AllowanceIssuedRow.tsx b/deploy-web/src/app/settings/authorizations/AllowanceIssuedRow.tsx similarity index 62% rename from deploy-web/src/components/settings/AllowanceIssuedRow.tsx rename to deploy-web/src/app/settings/authorizations/AllowanceIssuedRow.tsx index d9aced5d4..bc0ee492d 100644 --- a/deploy-web/src/components/settings/AllowanceIssuedRow.tsx +++ b/deploy-web/src/app/settings/authorizations/AllowanceIssuedRow.tsx @@ -1,14 +1,14 @@ +"use client"; import React, { ReactNode } from "react"; -import { IconButton, TableCell } from "@mui/material"; -import { CustomTableRow } from "../shared/CustomTable"; -import { Address } from "../shared/Address"; import { FormattedTime } from "react-intl"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/Delete"; import { AllowanceType } from "@src/types/grant"; -import { AKTAmount } from "../shared/AKTAmount"; import { coinToUDenom } from "@src/utils/priceUtils"; import { getAllowanceTitleByType } from "@src/utils/grants"; +import { TableCell, TableRow } from "@src/components/ui/table"; +import { Address } from "@src/components/shared/Address"; +import { AKTAmount } from "@src/components/shared/AKTAmount"; +import { Button } from "@src/components/ui/button"; +import { Bin, Edit } from "iconoir-react"; type Props = { allowance: AllowanceType; @@ -18,10 +18,8 @@ type Props = { }; export const AllowanceIssuedRow: React.FunctionComponent = ({ allowance, onEditAllowance, setDeletingAllowance }) => { - // const denomData = useDenomData(grant.authorization.spend_limit.denom); - return ( - + {getAllowanceTitleByType(allowance)}
    @@ -33,13 +31,13 @@ export const AllowanceIssuedRow: React.FunctionComponent = ({ allowance, - onEditAllowance(allowance)}> - - - setDeletingAllowance(allowance)}> - - + + - + ); }; diff --git a/deploy-web/src/components/wallet/AllowanceModal.tsx b/deploy-web/src/app/settings/authorizations/AllowanceModal.tsx similarity index 79% rename from deploy-web/src/components/wallet/AllowanceModal.tsx rename to deploy-web/src/app/settings/authorizations/AllowanceModal.tsx index eaabbbb19..540f69c22 100644 --- a/deploy-web/src/components/wallet/AllowanceModal.tsx +++ b/deploy-web/src/app/settings/authorizations/AllowanceModal.tsx @@ -1,45 +1,42 @@ +"use client"; import { useRef, useState } from "react"; import { useForm, Controller } from "react-hook-form"; -import { FormControl, TextField, Typography, Box, Alert, InputAdornment } from "@mui/material"; import { addYears, format } from "date-fns"; -import { makeStyles } from "tss-react/mui"; import { useWallet } from "@src/context/WalletProvider"; import { aktToUakt, coinToDenom } from "@src/utils/priceUtils"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; -import { LinkTo } from "../shared/LinkTo"; import { event } from "nextjs-google-analytics"; import { AnalyticsEvents } from "@src/utils/analytics"; import { AllowanceType } from "@src/types/grant"; -import { Popup } from "../shared/Popup"; import { useDenomData } from "@src/hooks/useWalletBalance"; import { uAktDenom } from "@src/utils/constants"; import { FormattedDate } from "react-intl"; - -const useStyles = makeStyles()(theme => ({ - formControl: { - marginBottom: "1rem" - } -})); +import { Popup } from "@src/components/shared/Popup"; +import { Alert } from "@src/components/ui/alert"; +import { LinkTo } from "@src/components/shared/LinkTo"; +import FormControl from "@mui/material/FormControl/FormControl"; +import TextField from "@mui/material/TextField/TextField"; +import InputAdornment from "@mui/material/InputAdornment/InputAdornment"; +import { EncodeObject } from "@cosmjs/proto-signing"; + +type AllowanceFormValues = { + amount: number; + expiration: string; + useDepositor: boolean; + granteeAddress: string; +}; type Props = { address: string; - editingAllowance?: AllowanceType; + editingAllowance?: AllowanceType | null; onClose: () => void; }; export const AllowanceModal: React.FunctionComponent = ({ editingAllowance, address, onClose }) => { - const formRef = useRef(null); + const formRef = useRef(null); const [error, setError] = useState(""); - const { classes } = useStyles(); const { signAndBroadcastTx } = useWallet(); - const { - handleSubmit, - control, - formState: { errors }, - watch, - clearErrors, - setValue - } = useForm({ + const { handleSubmit, control, watch, clearErrors, setValue } = useForm({ defaultValues: { amount: editingAllowance ? coinToDenom(editingAllowance.allowance.spend_limit[0]) : 0, expiration: format(addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm"), @@ -52,14 +49,14 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc const onDepositClick = event => { event.preventDefault(); - formRef.current.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); + formRef.current?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); }; - const onSubmit = async ({ amount }) => { + const onSubmit = async ({ amount, expiration, granteeAddress }: AllowanceFormValues) => { setError(""); clearErrors(); - const messages = []; + const messages: EncodeObject[] = []; const spendLimit = aktToUakt(amount); const expirationDate = new Date(expiration); @@ -87,7 +84,7 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc const onBalanceClick = () => { clearErrors(); - setValue("amount", denomData?.inputMax); + setValue("amount", denomData?.inputMax || 0); }; return ( @@ -106,7 +103,7 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc { label: "Grant", color: "secondary", - variant: "contained", + variant: "default", side: "right", disabled: !amount, onClick: onDepositClick @@ -118,20 +115,20 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc title="Authorize Fee Spending" >
    - - + +

    handleDocClick(ev, "https://docs.cosmos.network/v0.46/modules/feegrant/")}>Authorized Fee Spend allows users to authorize spend of a set number of tokens on fees from a source wallet to a destination, funded wallet. - +

    - +
    onBalanceClick()}> Balance: {denomData?.balance} {denomData?.label} - +
    - + = ({ editingAllowanc /> - + = ({ editingAllowanc /> - + = ({ editingAllowanc {!!amount && granteeAddress && ( - - + +

    This address will be able to spend up to {amount} AKT on transaction fees on your behalf ending on{" "} . - +

    )} - {error && {error}} + {error && {error}} ); }; - diff --git a/deploy-web/src/app/settings/authorizations/Authorizations.tsx b/deploy-web/src/app/settings/authorizations/Authorizations.tsx new file mode 100644 index 000000000..e2c93f71c --- /dev/null +++ b/deploy-web/src/app/settings/authorizations/Authorizations.tsx @@ -0,0 +1,321 @@ +"use client"; +import { PageContainer } from "@src/components/shared/PageContainer"; +import { SettingsLayout, SettingsTabs } from "@src/app/settings/SettingsLayout"; +import { Fieldset } from "@src/components/shared/Fieldset"; +import { useEffect, useState } from "react"; +import { useWallet } from "@src/context/WalletProvider"; +import { CustomTableHeader } from "@src/components/shared/CustomTable"; +import { Address } from "@src/components/shared/Address"; +import { GrantModal } from "@src/app/settings/authorizations/GrantModal"; +import { AllowanceType, GrantType } from "@src/types/grant"; +import { TransactionMessageData } from "@src/utils/TransactionMessageData"; +import { useAllowancesGranted, useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; +import { Popup } from "@src/components/shared/Popup"; +import { GranterRow } from "@src/app/settings/authorizations/GranterRow"; +import { GranteeRow } from "@src/app/settings/authorizations/GranteeRow"; +import { AllowanceModal } from "@src/app/settings/authorizations/AllowanceModal"; +import { AllowanceIssuedRow } from "@src/app/settings/authorizations/AllowanceIssuedRow"; +import { averageBlockTime } from "@src/utils/priceUtils"; +import { AllowanceGrantedRow } from "@src/app/settings/authorizations/AllowanceGrantedRow"; +import { Button } from "@src/components/ui/button"; +import { Bank } from "iconoir-react"; +import Spinner from "@src/components/shared/Spinner"; +import { Table, TableBody, TableHead, TableHeader, TableRow } from "@src/components/ui/table"; + +type Props = {}; + +type RefreshingType = "granterGrants" | "granteeGrants" | "allowancesIssued" | "allowancesGranted" | null; +const defaultRefetchInterval = 30 * 1000; +const refreshingInterval = 1000; + +export const Authorizations: React.FunctionComponent = ({}) => { + const { address, signAndBroadcastTx } = useWallet(); + const [editingGrant, setEditingGrant] = useState(null); + const [editingAllowance, setEditingAllowance] = useState(null); + const [showGrantModal, setShowGrantModal] = useState(false); + const [showAllowanceModal, setShowAllowanceModal] = useState(false); + const [deletingGrant, setDeletingGrant] = useState(null); + const [deletingAllowance, setDeletingAllowance] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(null); + const { data: granterGrants, isLoading: isLoadingGranterGrants } = useGranterGrants(address, { + refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval + }); + const { data: granteeGrants, isLoading: isLoadingGranteeGrants } = useGranteeGrants(address, { + refetchInterval: isRefreshing === "granteeGrants" ? refreshingInterval : defaultRefetchInterval + }); + const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued } = useAllowancesIssued(address, { + refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval + }); + const { data: allowancesGranted, isLoading: isLoadingAllowancesGranted } = useAllowancesGranted(address, { + refetchInterval: isRefreshing === "allowancesGranted" ? refreshingInterval : defaultRefetchInterval + }); + + useEffect(() => { + let timeout: NodeJS.Timeout; + if (isRefreshing) { + timeout = setTimeout(() => { + setIsRefreshing(null); + }, averageBlockTime * 1000); + } + + return () => { + if (timeout) { + clearTimeout(timeout); + } + }; + }, [isRefreshing]); + + async function onDeleteGrantConfirmed() { + if (!deletingGrant) return; + + const message = TransactionMessageData.getRevokeMsg(address, deletingGrant.grantee, deletingGrant.authorization["@type"]); + const response = await signAndBroadcastTx([message]); + + if (response) { + setIsRefreshing("granterGrants"); + setDeletingGrant(null); + } + } + + async function onDeleteAllowanceConfirmed() { + if (!deletingAllowance) return; + + const message = TransactionMessageData.getRevokeAllowanceMsg(address, deletingAllowance.grantee); + const response = await signAndBroadcastTx([message]); + + if (response) { + setIsRefreshing("allowancesIssued"); + setDeletingAllowance(null); + } + } + + function onCreateNewGrant() { + setEditingGrant(null); + setShowGrantModal(true); + } + + function onEditGrant(grant: GrantType) { + setEditingGrant(grant); + setShowGrantModal(true); + } + + function onGrantClose() { + setIsRefreshing("granterGrants"); + setShowGrantModal(false); + } + + function onCreateNewAllowance() { + setEditingAllowance(null); + setShowAllowanceModal(true); + } + + function onAllowanceClose() { + setIsRefreshing("allowancesIssued"); + setShowAllowanceModal(false); + } + + function onEditAllowance(allowance: AllowanceType) { + setEditingAllowance(allowance); + setShowAllowanceModal(true); + } + + return ( + + + +
    + } + > +

    + These authorizations allow you authorize other addresses to spend on deployments or deployment deposits using your funds. You can revoke these + authorizations at any time. +

    +
    + {isLoadingGranterGrants || !granterGrants ? ( +
    + +
    + ) : ( + <> + {granterGrants.length > 0 ? ( + + + + Grantee + Spending Limit + Expiration + + + + + + {granterGrants.map(grant => ( + + ))} + +
    + ) : ( +

    No authorizations given.

    + )} + + )} +
    + +
    + {isLoadingGranteeGrants || !granteeGrants ? ( +
    + +
    + ) : ( + <> + {granteeGrants.length > 0 ? ( + + + + Granter + Spending Limit + Expiration + + + + + {granteeGrants.map(grant => ( + + ))} + +
    + ) : ( +

    No authorizations received.

    + )} + + )} +
    + +
    +

    Tx Fee Authorizations

    + +
    + +

    + These authorizations allow you authorize other addresses to spend on transaction fees using your funds. You can revoke these authorizations at any + time. +

    + +
    + {isLoadingAllowancesIssued || !allowancesIssued ? ( +
    + +
    + ) : ( + <> + {allowancesIssued.length > 0 ? ( + + + + Type + Grantee + Spending Limit + Expiration + + + + + + {allowancesIssued.map(allowance => ( + + ))} + +
    + ) : ( +

    No allowances issued.

    + )} + + )} +
    + +
    + {isLoadingAllowancesGranted || !allowancesGranted ? ( +
    + +
    + ) : ( + <> + {allowancesGranted.length > 0 ? ( + + + + Type + Grantee + Spending Limit + Expiration + + + + + {allowancesGranted.map(allowance => ( + + ))} + +
    + ) : ( +

    No allowances received.

    + )} + + )} +
    + + {!!deletingGrant && ( + setDeletingGrant(null)} + onCancel={() => setDeletingGrant(null)} + onValidate={onDeleteGrantConfirmed} + enableCloseOnBackdropClick + > + Deleting grant to{" "} + +
    + {" "} + will revoke their ability to spend your funds on deployments. + + )} + {!!deletingAllowance && ( + setDeletingAllowance(null)} + onCancel={() => setDeletingAllowance(null)} + onValidate={onDeleteAllowanceConfirmed} + enableCloseOnBackdropClick + > + Deleting allowance to{" "} + +
    + {" "} + will revoke their ability to fees on your behalf. + + )} + {showGrantModal && } + {showAllowanceModal && } + + + ); +}; diff --git a/deploy-web/src/components/wallet/GrantModal.tsx b/deploy-web/src/app/settings/authorizations/GrantModal.tsx similarity index 80% rename from deploy-web/src/components/wallet/GrantModal.tsx rename to deploy-web/src/app/settings/authorizations/GrantModal.tsx index 51030c270..9c5af31a2 100644 --- a/deploy-web/src/components/wallet/GrantModal.tsx +++ b/deploy-web/src/app/settings/authorizations/GrantModal.tsx @@ -1,32 +1,38 @@ +"use client"; import { useRef, useState } from "react"; import { useForm, Controller } from "react-hook-form"; -import { FormControl, TextField, Typography, Box, Alert, Select, MenuItem, InputLabel } from "@mui/material"; import { addYears, format } from "date-fns"; -import { makeStyles } from "tss-react/mui"; import { useWallet } from "@src/context/WalletProvider"; import { aktToUakt, coinToDenom } from "@src/utils/priceUtils"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; -import { LinkTo } from "../shared/LinkTo"; import { event } from "nextjs-google-analytics"; import { AnalyticsEvents } from "@src/utils/analytics"; import { GrantType } from "@src/types/grant"; -import { Popup } from "../shared/Popup"; import { getUsdcDenom, useUsdcDenom } from "@src/hooks/useDenom"; import { denomToUdenom } from "@src/utils/mathHelpers"; import { useDenomData } from "@src/hooks/useWalletBalance"; import { uAktDenom } from "@src/utils/constants"; import { FormattedDate } from "react-intl"; import { handleDocClick } from "@src/utils/urlUtils"; - -const useStyles = makeStyles()(theme => ({ - formControl: { - marginBottom: "1rem" - } -})); +import { Popup } from "@src/components/shared/Popup"; +import { Alert } from "@src/components/ui/alert"; +import { LinkTo } from "@src/components/shared/LinkTo"; +import FormControl from "@mui/material/FormControl/FormControl"; +import InputLabel from "@mui/material/InputLabel/InputLabel"; +import Select from "@mui/material/Select/Select"; +import MenuItem from "@mui/material/MenuItem/MenuItem"; +import TextField from "@mui/material/TextField/TextField"; +type GrantFormValues = { + token: string; + amount: number; + expiration: string; + useDepositor: boolean; + granteeAddress: string; +}; type Props = { address: string; - editingGrant?: GrantType; + editingGrant?: GrantType | null; onClose: () => void; }; @@ -36,19 +42,11 @@ const supportedTokens = [ ]; export const GrantModal: React.FunctionComponent = ({ editingGrant, address, onClose }) => { - const formRef = useRef(null); + const formRef = useRef(null); const [error, setError] = useState(""); - const { classes } = useStyles(); const { signAndBroadcastTx } = useWallet(); const usdcDenom = useUsdcDenom(); - const { - handleSubmit, - control, - formState: { errors }, - watch, - clearErrors, - setValue - } = useForm({ + const { handleSubmit, control, watch, clearErrors, setValue } = useForm({ defaultValues: { token: editingGrant ? (editingGrant.authorization.spend_limit.denom === usdcDenom ? "usdc" : "akt") : "akt", amount: editingGrant ? coinToDenom(editingGrant.authorization.spend_limit) : 0, @@ -64,10 +62,10 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre const onDepositClick = event => { event.preventDefault(); - formRef.current.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); + formRef.current?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); }; - const onSubmit = async ({ amount }) => { + const onSubmit = async ({ amount, expiration, granteeAddress }: GrantFormValues) => { setError(""); clearErrors(); const spendLimit = token === "akt" ? aktToUakt(amount) : denomToUdenom(amount); @@ -90,7 +88,7 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre const onBalanceClick = () => { clearErrors(); - setValue("amount", denomData?.inputMax); + setValue("amount", denomData?.inputMax || 0); }; return ( @@ -109,7 +107,7 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre { label: "Grant", color: "secondary", - variant: "contained", + variant: "default", side: "right", disabled: !amount, onClick: onDepositClick @@ -121,8 +119,11 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre title="Authorize Spending" >
    - - + +

    handleDocClick( @@ -135,16 +136,16 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre {" "} allows users to authorize spend of a set number of tokens from a source wallet to a destination, funded wallet. The authorized spend is restricted to Akash deployment activities and the recipient of the tokens would not have access to those tokens for other operations. - +

    - +
    onBalanceClick()}> Balance: {denomData?.balance} {denomData?.label} - +
    - + Token = ({ editingGrant, addre /> - + = ({ editingGrant, addre /> - + = ({ editingGrant, addre {!!amount && granteeAddress && ( - - - This address will be able to spend up to {amount} {selectedToken.label} on your behalf ending on{" "} + +

    + This address will be able to spend up to {amount} {selectedToken?.label} on your behalf ending on{" "} . - +

    )} - {error && {error}} + {error && {error}} ); diff --git a/deploy-web/src/components/settings/GranteeRow.tsx b/deploy-web/src/app/settings/authorizations/GranteeRow.tsx similarity index 78% rename from deploy-web/src/components/settings/GranteeRow.tsx rename to deploy-web/src/app/settings/authorizations/GranteeRow.tsx index 8b3a53d7f..990071379 100644 --- a/deploy-web/src/components/settings/GranteeRow.tsx +++ b/deploy-web/src/app/settings/authorizations/GranteeRow.tsx @@ -1,12 +1,12 @@ +"use client"; import React, { ReactNode } from "react"; -import { TableCell } from "@mui/material"; -import { CustomTableRow } from "../shared/CustomTable"; -import { Address } from "../shared/Address"; import { FormattedTime } from "react-intl"; import { coinToUDenom } from "@src/utils/priceUtils"; import { GrantType } from "@src/types/grant"; -import { AKTAmount } from "../shared/AKTAmount"; import { useDenomData } from "@src/hooks/useWalletBalance"; +import { TableCell, TableRow } from "@src/components/ui/table"; +import { Address } from "@src/components/shared/Address"; +import { AKTAmount } from "@src/components/shared/AKTAmount"; type Props = { grant: GrantType; @@ -17,7 +17,7 @@ export const GranteeRow: React.FunctionComponent = ({ grant }) => { const denomData = useDenomData(grant.authorization.spend_limit.denom); return ( - +
    @@ -27,6 +27,6 @@ export const GranteeRow: React.FunctionComponent = ({ grant }) => { - + ); }; diff --git a/deploy-web/src/components/settings/GranterRow.tsx b/deploy-web/src/app/settings/authorizations/GranterRow.tsx similarity index 64% rename from deploy-web/src/components/settings/GranterRow.tsx rename to deploy-web/src/app/settings/authorizations/GranterRow.tsx index 703c4a0c3..6899f3a7f 100644 --- a/deploy-web/src/components/settings/GranterRow.tsx +++ b/deploy-web/src/app/settings/authorizations/GranterRow.tsx @@ -1,14 +1,14 @@ +"use client"; import React, { ReactNode } from "react"; -import { IconButton, TableCell } from "@mui/material"; -import { CustomTableRow } from "../shared/CustomTable"; -import { Address } from "../shared/Address"; import { FormattedTime } from "react-intl"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/Delete"; import { coinToUDenom } from "@src/utils/priceUtils"; import { GrantType } from "@src/types/grant"; -import { AKTAmount } from "../shared/AKTAmount"; import { useDenomData } from "@src/hooks/useWalletBalance"; +import { TableCell, TableRow } from "@src/components/ui/table"; +import { Address } from "@src/components/shared/Address"; +import { AKTAmount } from "@src/components/shared/AKTAmount"; +import { Button } from "@src/components/ui/button"; +import { Bin, Edit } from "iconoir-react"; type Props = { grant: GrantType; @@ -21,7 +21,7 @@ export const GranterRow: React.FunctionComponent = ({ children, grant, on const denomData = useDenomData(grant.authorization.spend_limit.denom); return ( - +
    @@ -32,13 +32,13 @@ export const GranterRow: React.FunctionComponent = ({ children, grant, on - onEditGrant(grant)}> - - - setDeletingGrant(grant)}> - - + + - + ); }; diff --git a/deploy-web/src/app/settings/authorizations/page.tsx b/deploy-web/src/app/settings/authorizations/page.tsx new file mode 100644 index 000000000..0c9d9da60 --- /dev/null +++ b/deploy-web/src/app/settings/authorizations/page.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { Metadata } from "next"; +import { Authorizations } from "./Authorizations"; + +export const metadata: Metadata = { + title: "Authorizations" +}; + +export default function AuthorizationsPage() { + return ; +} diff --git a/deploy-web/src/app/settings/page.tsx b/deploy-web/src/app/settings/page.tsx new file mode 100644 index 000000000..2ca340746 --- /dev/null +++ b/deploy-web/src/app/settings/page.tsx @@ -0,0 +1,10 @@ +import { Metadata } from "next"; +import { SettingsContainer } from "./SettingsContainer"; + +export const metadata: Metadata = { + title: "Settings" +}; + +export default function Settings() { + return ; +} diff --git a/deploy-web/src/components/certificates/CertificateDisplay.tsx b/deploy-web/src/components/certificates/CertificateDisplay.tsx deleted file mode 100644 index 568af07fc..000000000 --- a/deploy-web/src/components/certificates/CertificateDisplay.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useState } from "react"; -import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; -import RefreshIcon from "@mui/icons-material/Refresh"; -import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; -import WarningIcon from "@mui/icons-material/Warning"; -import AutorenewIcon from "@mui/icons-material/Autorenew"; -import CreateIcon from "@mui/icons-material/Create"; -import GetAppIcon from "@mui/icons-material/GetApp"; -import { useCertificate } from "../../context/CertificateProvider"; -import { ExportCertificate } from "./ExportCertificate"; -import { makeStyles } from "tss-react/mui"; -import { Box, Button, CircularProgress, IconButton, Menu, Paper, Tooltip, Typography, useTheme } from "@mui/material"; -import { CustomMenuItem } from "../shared/CustomMenuItem"; -import { useWallet } from "@src/context/WalletProvider"; -import CheckIcon from "@mui/icons-material/Check"; - -const useStyles = makeStyles()({ - warningIcon: { - marginLeft: ".5rem" - }, - tooltip: { - fontSize: "1rem" - } -}); - -export function CertificateDisplay() { - const [isExportingCert, setIsExportingCert] = useState(false); - const { - selectedCertificate, - isLocalCertMatching, - isLoadingCertificates, - loadValidCertificates, - localCert, - createCertificate, - isCreatingCert, - regenerateCertificate, - revokeCertificate - } = useCertificate(); - const { classes } = useStyles(); - const { address } = useWallet(); - const [anchorEl, setAnchorEl] = useState(null); - const theme = useTheme(); - - function handleMenuClick(ev) { - setAnchorEl(ev.currentTarget); - } - - const handleClose = () => { - setAnchorEl(null); - }; - - const onRegenerateCert = () => { - handleClose(); - - regenerateCertificate(); - }; - - const onRevokeCert = () => { - handleClose(); - - revokeCertificate(selectedCertificate); - }; - - return ( - <> - {address && ( - - - - {selectedCertificate ? ( - - Current certificate:{" "} - - {selectedCertificate.serial} - - - ) : ( - "No local certificate." - )} - - - {selectedCertificate && !isLocalCertMatching && ( - - - - )} - - - {!isLoadingCertificates && !selectedCertificate && ( - - - - )} - - loadValidCertificates(true)} - aria-label="refresh" - disabled={isLoadingCertificates} - size="small" - sx={{ marginLeft: "1rem" }} - > - {isLoadingCertificates ? : } - - - {selectedCertificate && ( - - - - - - )} - - )} - - {selectedCertificate && ( - - {/** If local, regenerate else create */} - {selectedCertificate.parsed === localCert?.certPem ? ( - onRegenerateCert()} icon={} text="Regenerate" /> - ) : ( - createCertificate()} icon={} text="Create" /> - )} - - onRevokeCert()} icon={} text="Revoke" /> - setIsExportingCert(true)} icon={} text="Export" /> - - )} - - {isExportingCert && setIsExportingCert(false)} />} - - ); -} - diff --git a/deploy-web/src/components/certificates/ExportCertificate.tsx b/deploy-web/src/components/certificates/ExportCertificate.tsx deleted file mode 100644 index 4c3618927..000000000 --- a/deploy-web/src/components/certificates/ExportCertificate.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect } from "react"; -import { Button, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Alert } from "@mui/material"; -import { makeStyles } from "tss-react/mui"; -import { useSelectedWalletFromStorage } from "@src/utils/walletUtils"; -import { CodeSnippet } from "../shared/CodeSnippet"; -import { event } from "nextjs-google-analytics"; -import { AnalyticsEvents } from "@src/utils/analytics"; - -const useStyles = makeStyles()(theme => ({ - label: { - fontWeight: "bold" - }, - dialogContent: { - padding: "1rem" - } -})); - -export function ExportCertificate(props) { - const { classes } = useStyles(); - const selectedWallet = useSelectedWalletFromStorage(); - - useEffect(() => { - async function init() { - event(AnalyticsEvents.EXPORT_CERTIFICATE, { - category: "certificates", - label: "Export certificate" - }); - } - - init(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - Export certificate - - {selectedWallet && selectedWallet.cert && selectedWallet.certKey ? ( - <> - - Cert - - - - Key - - - - ) : ( - - Unable to find local certificate. Meaning you have a certificate on chain but not in the tool. We suggest you regenerate a new one to be able to use - the tool properly. - - )} - - - - - - ); -} diff --git a/deploy-web/src/components/certificates/index.ts b/deploy-web/src/components/certificates/index.ts deleted file mode 100644 index 3071e92d6..000000000 --- a/deploy-web/src/components/certificates/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CertificateDisplay } from "./CertificateDisplay"; diff --git a/deploy-web/src/components/layout/ColorModeSelect.tsx b/deploy-web/src/components/layout/ColorModeSelect.tsx deleted file mode 100644 index 600215219..000000000 --- a/deploy-web/src/components/layout/ColorModeSelect.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from "@mui/material"; -import { useDarkMode } from "next-dark-mode"; - -type Props = {}; - -export const ColorModeSelect: React.FunctionComponent = () => { - const { darkModeActive, autoModeActive, switchToDarkMode, switchToLightMode, switchToAutoMode } = useDarkMode(); - const [mode, setMode] = useState("auto"); - - useEffect(() => { - const _mode = autoModeActive ? "auto" : darkModeActive ? "dark" : "light"; - setMode(_mode); - }, []); - - const onModeChange = (event: SelectChangeEvent) => { - const newMode = event.target.value; - setMode(newMode); - - switch (newMode) { - case "dark": - switchToDarkMode(); - break; - case "light": - switchToLightMode(); - break; - - case "auto": - default: - switchToAutoMode(); - break; - } - }; - - return ( - - Theme - - - ); -}; diff --git a/deploy-web/src/components/settings/SettingsLayout.tsx b/deploy-web/src/components/settings/SettingsLayout.tsx deleted file mode 100644 index 60078f408..000000000 --- a/deploy-web/src/components/settings/SettingsLayout.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { ReactNode, useEffect, useState } from "react"; -import Box from "@mui/material/Box"; -import { ErrorBoundary } from "react-error-boundary"; -import { ErrorFallback } from "../shared/ErrorFallback"; -import { Container, Tab, Tabs, Typography, useTheme } from "@mui/material"; -import { makeStyles } from "tss-react/mui"; -import { UrlService } from "@src/utils/urlUtils"; -import { useRouter } from "next/router"; - -export enum SettingsTabs { - GENERAL = 1, - AUTHORIZATIONS = 2 -} - -type Props = { - page: SettingsTabs; - children?: ReactNode; - title: string; - headerActions?: ReactNode; -}; - -const useStyles = makeStyles()(theme => ({ - tabsRoot: { - minHeight: "36px", - borderBottom: `1px solid ${theme.palette.mode === "dark" ? theme.palette.grey[900] : theme.palette.grey[300]}`, - "& button": { - minHeight: "36px" - } - }, - selectedTab: { - fontWeight: "bold" - }, - tabsContainer: { - justifyContent: "center" - }, - titleContainer: { - paddingBottom: "0.5rem", - display: "flex", - alignItems: "center", - flexWrap: "wrap" - } -})); - -const SettingsLayout: React.FunctionComponent = ({ children, page, title, headerActions }) => { - const theme = useTheme(); - const { classes } = useStyles(); - const router = useRouter(); - - const handleTabChange = (event: React.SyntheticEvent, newValue: SettingsTabs) => { - switch (newValue) { - case SettingsTabs.AUTHORIZATIONS: - router.push(UrlService.settingsAuthorizations()); - break; - case SettingsTabs.GENERAL: - default: - router.push(UrlService.settings()); - break; - } - }; - - return ( - <> - - - - - - - - - {title} - - {headerActions} - - - - {children} - - ); -}; - -export default SettingsLayout; diff --git a/deploy-web/src/components/shared/CodeSnippet.tsx b/deploy-web/src/components/shared/CodeSnippet.tsx index b138ea35d..00809f7b8 100644 --- a/deploy-web/src/components/shared/CodeSnippet.tsx +++ b/deploy-web/src/components/shared/CodeSnippet.tsx @@ -1,54 +1,32 @@ -import { lighten, Box, IconButton } from "@mui/material"; -import FileCopyIcon from "@mui/icons-material/FileCopy"; -import { useSnackbar } from "notistack"; +"use client"; import { useRef } from "react"; -import { makeStyles } from "tss-react/mui"; import { copyTextToClipboard } from "@src/utils/copyClipboard"; -import { Snackbar } from "./Snackbar"; import { selectText } from "@src/utils/stringUtils"; -import { grey } from "@mui/material/colors"; +import { useToast } from "../ui/use-toast"; +import { Button } from "../ui/button"; +import { Copy } from "iconoir-react"; -const useStyles = makeStyles()(theme => ({ - root: { - position: "relative", - padding: "1rem", - borderRadius: theme.spacing(0.5), - backgroundColor: theme.palette.mode === "dark" ? grey[900] : lighten("#000", 0.9), - fontSize: ".9rem" - }, - actions: { - position: "absolute", - width: "100%", - top: 0, - left: 0, - padding: theme.spacing(0.5), - display: "flex", - justifyContent: "flex-end" - } -})); - -export const CodeSnippet = ({ code }) => { - const { classes } = useStyles(); - const { enqueueSnackbar } = useSnackbar(); - const codeRef = useRef(); +export const CodeSnippet = ({ code }: React.PropsWithChildren<{ code: string }>) => { + const { toast } = useToast(); + const codeRef = useRef(null); const onCopyClick = () => { copyTextToClipboard(code); - enqueueSnackbar(, { variant: "success", autoHideDuration: 1500 }); + toast({ title: "Copied to clipboard!", variant: "success" }); }; const onCodeClick = () => { - selectText(codeRef.current); + if (codeRef?.current) selectText(codeRef.current); }; return ( -
    -      
    -        
    -          
    -        
    -      
    -      
    +    
    +      
    + +
    + {code}
    diff --git a/deploy-web/src/components/shared/ConnectWallet.tsx b/deploy-web/src/components/shared/ConnectWallet.tsx index 6060bb5c1..7e3949561 100644 --- a/deploy-web/src/components/shared/ConnectWallet.tsx +++ b/deploy-web/src/components/shared/ConnectWallet.tsx @@ -9,16 +9,8 @@ type Props = { export const ConnectWallet: React.FunctionComponent = ({ text }) => { return ( -
    -

    - {text} -

    +
    +

    {text}

    ); diff --git a/deploy-web/src/components/shared/CustomDropdownLinkItem.tsx b/deploy-web/src/components/shared/CustomDropdownLinkItem.tsx index 9180faa4c..4504291e0 100644 --- a/deploy-web/src/components/shared/CustomDropdownLinkItem.tsx +++ b/deploy-web/src/components/shared/CustomDropdownLinkItem.tsx @@ -1,3 +1,4 @@ +"use client"; import React from "react"; import { DropdownMenuIconItem } from "../ui/dropdown-menu"; import { cn } from "@src/utils/styleUtils"; diff --git a/deploy-web/src/components/shared/Fieldset.tsx b/deploy-web/src/components/shared/Fieldset.tsx index f4902213f..676b3ffcd 100644 --- a/deploy-web/src/components/shared/Fieldset.tsx +++ b/deploy-web/src/components/shared/Fieldset.tsx @@ -1,38 +1,23 @@ -import { makeStyles } from "tss-react/mui"; -import { udenomToDenom } from "@src/utils/mathHelpers"; -import { FormattedNumber, FormattedNumberParts } from "react-intl"; -import { Box, Paper, Typography, useTheme } from "@mui/material"; +"use client"; import React from "react"; -import { usePricing } from "@src/context/PricingProvider"; -import { AKTLabel } from "./AKTLabel"; -import { customColors } from "@src/utils/colors"; +import { Card, CardContent } from "../ui/card"; type Props = { label: string; + className?: string; children: React.ReactNode; }; -export const Fieldset: React.FunctionComponent = ({ label, children }) => { - const theme = useTheme(); - +export const Fieldset: React.FunctionComponent = ({ label, className = "", children }) => { return ( - - - {label} - + + +
    +

    {label}

    +
    - {children} -
    +
    {children}
    + + ); }; diff --git a/deploy-web/src/components/shared/LabelValue.tsx b/deploy-web/src/components/shared/LabelValue.tsx index e7c0cb64f..6f708928a 100644 --- a/deploy-web/src/components/shared/LabelValue.tsx +++ b/deploy-web/src/components/shared/LabelValue.tsx @@ -1,55 +1,23 @@ -import { Box } from "@mui/material"; -import { makeStyles } from "tss-react/mui"; +"use client"; -const useStyles = makeStyles()(theme => ({ - infoRow: { - display: "flex", - marginBottom: "1rem", - "&:last-child": { - marginBottom: 0 - }, - [theme.breakpoints.down("sm")]: { - flexDirection: "column", - alignItems: "flex-start" - } - }, - label: { - fontWeight: "bold", - flexShrink: 0, - wordBreak: "break-all", - color: theme.palette.grey[600], - display: "flex", - alignItems: "center", - paddingRight: ".5rem" - }, - value: { - wordBreak: "break-all", - overflowWrap: "anywhere", - flexGrow: 1, - [theme.breakpoints.down("sm")]: { - width: "100%" - } - } -})); +import { cn } from "@src/utils/styleUtils"; type LabelValueProps = { - label: any; - value?: any; + label?: string | React.ReactNode; + value?: string | React.ReactNode; labelWidth?: string | number; - - // All other props - [x: string]: any; + className?: string; }; -export const LabelValue: React.FunctionComponent = ({ label, value, labelWidth = "15rem", ...rest }) => { - const { classes } = useStyles(); - +export const LabelValue: React.FunctionComponent = ({ label, value, labelWidth = "15rem", className = "" }) => { return ( - - - {label} - - {!!value &&
    {value}
    } -
    +
    + {label && ( +
    + {label} +
    + )} + {value !== undefined &&
    {value}
    } +
    ); }; diff --git a/deploy-web/src/components/shared/SelectNetworkModal.tsx b/deploy-web/src/components/shared/SelectNetworkModal.tsx deleted file mode 100644 index b4c8efd38..000000000 --- a/deploy-web/src/components/shared/SelectNetworkModal.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { Alert, Box, Chip, List, ListItemButton, ListItemIcon, ListItemText, Radio, Typography } from "@mui/material"; -import { mainnetId } from "@src/utils/constants"; -import { useState } from "react"; -import { makeStyles } from "tss-react/mui"; -import { useSettings } from "../../context/SettingsProvider"; -import { Popup } from "./Popup"; -import { networks } from "@src/store/networkStore"; - -const useStyles = makeStyles()(theme => ({ - experimentalChip: { - height: "16px", - marginLeft: "1rem", - fontSize: ".7rem", - fontWeight: "bold" - }, - version: { - fontWeight: "bold" - }, - alert: { - marginBottom: "1rem" - } -})); - -export const SelectNetworkModal = ({ onClose }) => { - const { classes } = useStyles(); - const { selectedNetworkId } = useSettings(); - const [localSelectedNetworkId, setLocalSelectedNetworkId] = useState(selectedNetworkId); - - const handleSelectNetwork = network => { - setLocalSelectedNetworkId(network.id); - }; - - const handleSaveChanges = () => { - if (selectedNetworkId !== localSelectedNetworkId) { - // Set in the settings and local storage - localStorage.setItem("selectedNetworkId", localSelectedNetworkId); - // Reset the ui to reload the settings for the currently selected network - - location.reload(); - } else { - onClose(); - } - }; - - return ( - - - {networks.map(network => { - return ( - handleSelectNetwork(network)} disabled={!network.enabled}> - - - - - - {network.title} - {" - "} - - {network.version} - - - {network.id !== mainnetId && } - - } - secondary={network.description} - /> - - ); - })} - - - {localSelectedNetworkId !== mainnetId && ( - - - Warning - - - Changing networks will restart the app and some features are experimental. - - )} - - ); -}; diff --git a/deploy-web/src/components/ui/checkbox.tsx b/deploy-web/src/components/ui/checkbox.tsx index ea03eb612..f98ecab4c 100644 --- a/deploy-web/src/components/ui/checkbox.tsx +++ b/deploy-web/src/components/ui/checkbox.tsx @@ -38,11 +38,11 @@ const CheckboxWithLabel = React.forwardRef< ); return ( -