diff --git a/apps/gateway-ui/src/App.tsx b/apps/gateway-ui/src/App.tsx index 3532adea7..4d4df4a4c 100644 --- a/apps/gateway-ui/src/App.tsx +++ b/apps/gateway-ui/src/App.tsx @@ -91,7 +91,7 @@ export const App = React.memo(function Admin(): JSX.Element { return ( - + {error ? ( {t('common.error')} diff --git a/apps/gateway-ui/src/components/BalanceCard.tsx b/apps/gateway-ui/src/components/BalanceCard.tsx index ffd9f4000..72baa2665 100644 --- a/apps/gateway-ui/src/components/BalanceCard.tsx +++ b/apps/gateway-ui/src/components/BalanceCard.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { Box, Flex, Text, useTheme } from '@chakra-ui/react'; +import { Text, useTheme } from '@chakra-ui/react'; import { useTranslation } from '@fedimint/utils'; -import { GatewayCard } from '.'; +import { GatewayCard } from './GatewayCard'; interface BalanceCardProps { balance_msat: number; @@ -15,37 +15,18 @@ export const BalanceCard = React.memo(function BalanceCard( const theme = useTheme(); return ( - - - - - {t('balance-card.card_header')} - - - {t('balance-card.sentence')} - - - - - {balance_msat} - - - - + + + {balance_msat} + + ); }); diff --git a/apps/gateway-ui/src/components/DepositCard.tsx b/apps/gateway-ui/src/components/DepositCard.tsx index 54a59bffc..ad1380903 100644 --- a/apps/gateway-ui/src/components/DepositCard.tsx +++ b/apps/gateway-ui/src/components/DepositCard.tsx @@ -44,95 +44,81 @@ export const DepositCard = React.memo(function DepositCard({ }, [address, federationId, gateway]); return ( - - - - {t('deposit-card.card_header')} - - - {t('deposit-card.sentence-one')} - - - - {address ? ( + + + + {address ? ( + + {address} + + ) : ( + + {error} + + )} + + {hasCopied ? ( - {address} + {t('common.copied')} ) : ( - - {error} - - )} - - {hasCopied ? ( - - {t('common.copied')} - - ) : ( - - )} - - - - {address && ( - + )} - - - - {t('deposit-card.mempool_deposit_link_text')} - - + + {address && ( + + )} + + + + {t('deposit-card.mempool_deposit_link_text')} + + + ); }); diff --git a/apps/gateway-ui/src/components/GatewayCard.tsx b/apps/gateway-ui/src/components/GatewayCard.tsx index f2211d9e2..362b20f7f 100644 --- a/apps/gateway-ui/src/components/GatewayCard.tsx +++ b/apps/gateway-ui/src/components/GatewayCard.tsx @@ -1,37 +1,30 @@ import React from 'react'; -import { Flex, Stack, useTheme } from '@chakra-ui/react'; +import { Card, CardBody, CardHeader, Text } from '@chakra-ui/react'; export interface GatewayCardProps { + title: string; + description?: string; children: React.ReactNode; } export const GatewayCard = React.memo(function GatewayCard({ + title, + description, children, }: GatewayCardProps) { - const theme = useTheme(); - return ( - - - {children} - - - + + + + {title} + + {description && ( + + {description} + + )} + + {children} + ); }); diff --git a/apps/gateway-ui/src/components/InfoCard.tsx b/apps/gateway-ui/src/components/InfoCard.tsx index e6e65f631..0bcf59aba 100644 --- a/apps/gateway-ui/src/components/InfoCard.tsx +++ b/apps/gateway-ui/src/components/InfoCard.tsx @@ -7,7 +7,7 @@ import { Box, useClipboard, } from '@chakra-ui/react'; -import { useTranslation } from '@fedimint/utils'; +import { formatEllipsized, useTranslation } from '@fedimint/utils'; import { GatewayCard } from '.'; import { ReactComponent as CopyIcon } from '../assets/svgs/copy.svg'; import { ReactComponent as LinkIcon } from '../assets/svgs/linkIcon.svg'; @@ -26,16 +26,8 @@ export const InfoCard = React.memo(function InfoCard( const { onCopy, hasCopied } = useClipboard(nodeId); return ( - - - {t('info-card.card_header')} - - + + - {nodeId} + {formatEllipsized(nodeId)} {hasCopied ? ( diff --git a/apps/gateway-ui/src/components/WithdrawCard.tsx b/apps/gateway-ui/src/components/WithdrawCard.tsx index 9f349b396..abfb1d784 100644 --- a/apps/gateway-ui/src/components/WithdrawCard.tsx +++ b/apps/gateway-ui/src/components/WithdrawCard.tsx @@ -96,35 +96,16 @@ export const WithdrawCard = React.memo(function WithdrawCard({ }); }; + const description = `${t('withdraw-card.total_bitcoin')} ${ + withdrawObject.amount / 100000 + } ${t('common.btc')}`; + return ( - - - - - {t('withdraw-card.card-header')} - - - {t('withdraw-card.total_bitcoin')} {withdrawObject.amount / 100000}{' '} - {t('common.btc')} - - - {t('withdraw-card.withdraw_all')} - - + <> + handleInputChange(e)} name='amount' /> + + + {t('withdraw-card.withdraw_all')} + )} - + ); }); diff --git a/apps/guardian-ui/src/App.tsx b/apps/guardian-ui/src/App.tsx index 9fd6db6fd..babb9eaf5 100644 --- a/apps/guardian-ui/src/App.tsx +++ b/apps/guardian-ui/src/App.tsx @@ -48,7 +48,7 @@ export const App = React.memo(function App() { if (state.status === Status.Admin) { return ( - + diff --git a/apps/guardian-ui/src/admin/FederationAdmin.tsx b/apps/guardian-ui/src/admin/FederationAdmin.tsx index cedc338f1..dba50199e 100644 --- a/apps/guardian-ui/src/admin/FederationAdmin.tsx +++ b/apps/guardian-ui/src/admin/FederationAdmin.tsx @@ -1,237 +1,79 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { - Flex, - Card, - CardBody, - CardHeader, - Table, - Tbody, - Tr, - Td, - Thead, - Th, - Box, - Icon, - Text, - useTheme, -} from '@chakra-ui/react'; +import React, { useEffect, useState } from 'react'; +import { Flex, Box, Icon, Text, useTheme, Heading } from '@chakra-ui/react'; import { CopyInput } from '@fedimint/ui'; import { useTranslation } from '@fedimint/utils'; import { useAdminContext } from '../hooks'; -import { - ConfigResponse, - Gateway, - PeerStatus, - StatusResponse, - Versions, -} from '../types'; -import { AdminMain } from '../components/AdminMain'; -import { ConnectedNodes } from '../components/ConnectedNodes'; -import { LightningModuleRpc } from '../GuardianApi'; +import { ConfigResponse, StatusResponse } from '../types'; +import { GatewaysCard } from '../components/GatewaysCard'; import { ReactComponent as CopyIcon } from '../assets/svgs/copy.svg'; -import { Pill } from '../components/Pill'; - -interface PeerData { - id: number; - name: string; - status: PeerStatus; -} +import { GuardiansCard } from '../components/GuardiansCard'; +import { FederationInfoCard } from '../components/FederationInfoCard'; +import { BitcoinNodeCard } from '../components/BitcoinNodeCard'; +import { BalanceCard } from '../components/BalanceCard'; export const FederationAdmin: React.FC = () => { const theme = useTheme(); const { api } = useAdminContext(); - const [versions, setVersions] = useState(); - const [epochCount, setEpochCount] = useState(); const [status, setStatus] = useState(); const [inviteCode, setInviteCode] = useState(''); const [config, setConfig] = useState(); - const [gateways, setGateways] = useState([]); const { t } = useTranslation(); useEffect(() => { - api.version().then(setVersions).catch(console.error); - api.fetchEpochCount().then(setEpochCount).catch(console.error); // TODO: poll server status api.status().then(setStatus).catch(console.error); api.inviteCode().then(setInviteCode).catch(console.error); }, [api]); useEffect(() => { - inviteCode && api.config(inviteCode).then(setConfig).catch(console.error); + if (!inviteCode) return; + api.config(inviteCode).then(setConfig).catch(console.error); }, [inviteCode, api]); - useEffect(() => { - if (config) { - for (const [key, val] of Object.entries(config.client_config.modules)) { - if (val.kind === 'ln') { - api - .moduleApiCall( - Number(key), - LightningModuleRpc.listGateways - ) - .then(setGateways) - .catch(console.error); - return; - } - } - } - }, [config, api]); - - const [guardiansStatusText, statusType] = useMemo(() => { - const online = status?.federation ? status.federation.peers_online + 1 : 1; - const offline = status?.federation ? status.federation.peers_offline : 0; - const totalPeers = online + offline; - const onlinePercentage = online / totalPeers; - const statusType: 'success' | 'warning' | 'error' = - onlinePercentage === 1 - ? 'success' - : onlinePercentage >= 2 / 3 - ? 'warning' - : 'error'; - const guardiansStatusText = `${online} / ${totalPeers}`; - return [guardiansStatusText, statusType]; - }, [status]); - - const data = useMemo(() => { - if (status && status.federation && config) { - const peerDataArray: PeerData[] = []; - for (const [id, federationStatus] of Object.entries( - status.federation.status_by_peer - )) { - const numericId = parseInt(id, 10); - const endpoint = config.client_config.api_endpoints[numericId]; - if (endpoint) { - peerDataArray.push({ - id: numericId, - name: endpoint.name, - status: federationStatus, - }); - } - } - return peerDataArray; - } - }, [status, config]); - - const apiVersion = versions?.core.api.length - ? `${versions.core.api[0].major}.${versions.core.api[0].minor}` - : ''; - const consensusVersion = - versions?.core.core_consensus !== undefined - ? `${versions.core.core_consensus}` - : ''; - return ( - - - + + + + {config?.client_config.meta.federation_name} + + - {config?.client_config.meta.federation_name} + {t('federation-dashboard.invite-members')} - - {t('federation-dashboard.placeholder-fed-description')} + } + size='sm' + /> + + {t('federation-dashboard.invite-members-prompt')} - - - - - - - - {t('federation-dashboard.invite-members')} - - } - /> - - {t('federation-dashboard.invite-members-prompt')} - - + + + + + + + - - - - {t('federation-dashboard.fed-info.label')} - - - - - - - - - - - - - - - - - - - - -
- {t('federation-dashboard.fed-info.your-status-label')} - {status?.server}
- {t('federation-dashboard.fed-info.epoch-count-label')} - {epochCount}
- {t('federation-dashboard.fed-info.api-version-label')} - {apiVersion}
- {t( - 'federation-dashboard.fed-info.consensus-version-label' - )} - {consensusVersion}
-
-
- - {t('federation-dashboard.peer-info.label')} - - - - - - - - - - - {data && - data?.map((x) => ( - - - - - - ))} - -
{t('federation-dashboard.peer-info.id-label')}{t('federation-dashboard.peer-info.name-label')}{t('federation-dashboard.peer-info.status-label')}
{x.id}{x.name}{x.status.connection_status}
-
-
-
- + +
); diff --git a/apps/guardian-ui/src/components/AdminChart.tsx b/apps/guardian-ui/src/components/AdminChart.tsx deleted file mode 100644 index ef9f716dc..000000000 --- a/apps/guardian-ui/src/components/AdminChart.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { FC } from 'react'; -import { Box, Flex, Stack, Text, theme } from '@chakra-ui/react'; - -interface TabProps { - text: string; - active: boolean; - onClick: () => void; - style?: React.CSSProperties; -} - -const Tab: FC = ({ text, active, onClick, style }) => { - return ( - - - {text} - - - ); -}; - -const PeriodicChartTab = () => { - return ( - - - - - - ); -}; - -const CurrencyChartTab = () => { - return ( - - - - - - ); -}; - -export const AdminChart = () => { - return ( - - - - - - - ); -}; diff --git a/apps/guardian-ui/src/components/AdminMain.tsx b/apps/guardian-ui/src/components/AdminMain.tsx deleted file mode 100644 index 68c518e87..000000000 --- a/apps/guardian-ui/src/components/AdminMain.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { FC } from 'react'; -import { Box, Center, Flex, Text, useTheme } from '@chakra-ui/react'; -import { ReactComponent as BitcoinIcon } from '../assets/svgs/bitcoin-white.svg'; -import { ReactComponent as EcashIcon } from '../assets/svgs/ecash.svg'; -import { ReactComponent as BankNotesIcon } from '../assets/svgs/banknotes.svg'; -import { ReactComponent as InfoIcon } from '../assets/svgs/info.svg'; - -interface FederationBalanceProps { - title: string; - amount: number; - icon: React.ReactElement< - React.SVGProps & { - title?: string | undefined; - } - >; -} - -const FederationBalance: FC = ({ - title, - amount, - icon, -}) => { - const theme = useTheme(); - - return ( - - -
- {icon} -
- - - {title} - - - {amount} - - -
- -
- ); -}; - -export const AdminMain = () => { - const theme = useTheme(); - - return ( - - - - Community Custody - - {} - - - } - /> - } - /> - } - /> - - - ); -}; diff --git a/apps/guardian-ui/src/components/BalanceCard.tsx b/apps/guardian-ui/src/components/BalanceCard.tsx new file mode 100644 index 000000000..c5a2d8cee --- /dev/null +++ b/apps/guardian-ui/src/components/BalanceCard.tsx @@ -0,0 +1,33 @@ +import { Card, CardBody, CardHeader, Text } from '@chakra-ui/react'; +import { KeyValues } from '@fedimint/ui'; +import { useTranslation } from '@fedimint/utils'; +import React, { useMemo } from 'react'; + +export const BalanceCard: React.FC = () => { + const { t } = useTranslation(); + + const keyValues = useMemo( + () => [ + { + key: 'bitcoin', + label: t('common.bitcoin'), + // TODO: Use `audit` api to fill this in + value: '0.00000000', + }, + ], + [t] + ); + + return ( + + + + {t('federation-dashboard.balance.label')} + + + + + + + ); +}; diff --git a/apps/guardian-ui/src/components/BitcoinNodeCard.tsx b/apps/guardian-ui/src/components/BitcoinNodeCard.tsx new file mode 100644 index 000000000..c2c93a0ab --- /dev/null +++ b/apps/guardian-ui/src/components/BitcoinNodeCard.tsx @@ -0,0 +1,44 @@ +import { Card, CardBody, CardHeader, Text } from '@chakra-ui/react'; +import { useTranslation } from '@fedimint/utils'; +import React, { useMemo } from 'react'; +import { ConfigResponse } from '../types'; +import { KeyValues } from '@fedimint/ui'; + +interface Props { + config: ConfigResponse | undefined; +} + +export const BitcoinNodeCard: React.FC = () => { + const { t } = useTranslation(); + + // TODO: Populate values from config.client_config.modules.config + // It's currently mysteriously hex encoded + const keyValues = useMemo( + () => [ + { + key: 'url', + label: t('federation-dashboard.bitcoin-node.url-label'), + value: t('common.unknown'), + }, + { + key: 'blockHeight', + label: t('federation-dashboard.bitcoin-node.block-height-label'), + value: t('common.unknown'), + }, + ], + [t] + ); + + return ( + + + + {t('federation-dashboard.bitcoin-node.label')} + + + + + + + ); +}; diff --git a/apps/guardian-ui/src/components/ConnectedNodes.tsx b/apps/guardian-ui/src/components/ConnectedNodes.tsx deleted file mode 100644 index 36d7ca0e5..000000000 --- a/apps/guardian-ui/src/components/ConnectedNodes.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { CSSProperties, FC } from 'react'; -import { Box, Center, Flex, Stack, Text, useTheme } from '@chakra-ui/react'; -import { ReactComponent as LightningIcon } from '../assets/svgs/lightningIcon.svg'; -import { Fees, Gateway } from '../types'; - -interface ConnectedNodesProps { - gateways: Gateway[]; -} - -export const ConnectedNodes: FC = ({ gateways }) => { - return ( - <> - {gateways.map((gateway: Gateway) => ( - - ))} - - ); -}; - -interface LightningNodeProps { - nodeName: string; - nodeId: string; - incomingFees: Fees; - outgoingFees: Fees; -} - -const LightningNode: FC = ({ - nodeName, - nodeId, - incomingFees, - outgoingFees, -}) => { - const theme = useTheme(); - - const titleStyles: CSSProperties = { - color: theme.colors.gray[900], - fontWeight: '600', - }; - - const subTextStyles: CSSProperties = { - color: theme.colors.gray[500], - fontWeight: '500', - }; - - return ( - - -
- -
- - - {nodeName} - - - - Node ID - {nodeId} - - - - Fees - {`Incoming: ${incomingFees.base_msat} sats + (${incomingFees.proportional_millionths} ppm)`} - {`Outgoing: ${outgoingFees.base_msat} sats + (${outgoingFees.proportional_millionths} ppm)`} - - - -
- - - - View on amboss.space - - -
- ); -}; diff --git a/apps/guardian-ui/src/components/FederationInfoCard.tsx b/apps/guardian-ui/src/components/FederationInfoCard.tsx new file mode 100644 index 000000000..0f8731027 --- /dev/null +++ b/apps/guardian-ui/src/components/FederationInfoCard.tsx @@ -0,0 +1,70 @@ +import { Card, CardBody, CardHeader, Text } from '@chakra-ui/react'; +import { useTranslation } from '@fedimint/utils'; +import React, { useEffect, useMemo, useState } from 'react'; +import { StatusResponse, Versions } from '../types'; +import { useAdminContext } from '../hooks'; +import { KeyValues } from '@fedimint/ui'; + +interface Props { + status: StatusResponse | undefined; +} + +export const FederationInfoCard: React.FC = ({ status }) => { + const { t } = useTranslation(); + const { api } = useAdminContext(); + const [versions, setVersions] = useState(); + const [epochCount, setEpochCount] = useState(); + + const serverStatus = status?.server || ''; + const apiVersion = versions?.core.api.length + ? `${versions.core.api[0].major}.${versions.core.api[0].minor}` + : ''; + const consensusVersion = + versions?.core.core_consensus !== undefined + ? `${versions.core.core_consensus}` + : ''; + + useEffect(() => { + api.version().then(setVersions).catch(console.error); + api.fetchEpochCount().then(setEpochCount).catch(console.error); + }, [api]); + + const keyValues = useMemo( + () => [ + { + key: 'status', + label: t('federation-dashboard.fed-info.your-status-label'), + value: serverStatus, + }, + { + key: 'epochCount', + label: t('federation-dashboard.fed-info.epoch-count-label'), + value: epochCount, + }, + { + key: 'apiVersion', + label: t('federation-dashboard.fed-info.api-version-label'), + value: apiVersion, + }, + { + key: 'consensusVersion', + label: t('federation-dashboard.fed-info.consensus-version-label'), + value: consensusVersion, + }, + ], + [t, serverStatus, epochCount, apiVersion, consensusVersion] + ); + + return ( + + + + {t('federation-dashboard.fed-info.label')} + + + + + + + ); +}; diff --git a/apps/guardian-ui/src/components/GatewaysCard.tsx b/apps/guardian-ui/src/components/GatewaysCard.tsx new file mode 100644 index 000000000..7e1f3e376 --- /dev/null +++ b/apps/guardian-ui/src/components/GatewaysCard.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { + Card, + CardBody, + CardHeader, + Flex, + Link, + Text, + useTheme, +} from '@chakra-ui/react'; +import { ConfigResponse, Gateway } from '../types'; +import { Table, TableColumn, TableRow } from '@fedimint/ui'; +import { useTranslation, formatEllipsized } from '@fedimint/utils'; +import { useAdminContext } from '../hooks'; +import { LightningModuleRpc } from '../GuardianApi'; + +type TableKey = 'nodeId' | 'gatewayId' | 'fee'; + +interface GatewaysCardProps { + config: ConfigResponse | undefined; +} + +export const GatewaysCard: React.FC = ({ config }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { api } = useAdminContext(); + const [gateways, setGateways] = useState([]); + + const lnModuleId = config + ? Object.entries(config.client_config.modules).find( + (m) => m[1].kind === 'ln' + )?.[0] + : undefined; + + useEffect(() => { + if (!lnModuleId) return; + api + .moduleApiCall( + Number(lnModuleId), + LightningModuleRpc.listGateways + ) + .then(setGateways) + .catch(console.error); + }, [config, api, lnModuleId]); + + const columns: TableColumn[] = useMemo( + () => [ + { + key: 'nodeId', + heading: t('federation-dashboard.gateways.node-id-label'), + }, + { + key: 'gatewayId', + heading: t('federation-dashboard.gateways.gateway-id-label'), + }, + { + key: 'fee', + heading: t('federation-dashboard.gateways.fee-label'), + }, + ], + [t] + ); + + const rows: TableRow[] = useMemo( + () => + gateways.map(({ gateway_id, node_pub_key, fees }) => { + const feePct = fees.proportional_millionths / 10000; + const fee = [ + fees.base_msat ? `${fees.base_msat} sats +` : '', + `${fees.proportional_millionths} ppm`, + feePct ? `(${feePct}%)` : '', + ] + .filter(Boolean) + .join(' '); + return { + key: gateway_id, + nodeId: ( + + {formatEllipsized(node_pub_key)} + + + {t('federation-dashboard.gateways.view-on-site', { + site: 'amboss.space', + })} + + + + ), + gatewayId: formatEllipsized(gateway_id), + fee: fee, + outgoingFee: fee, + }; + }), + [gateways, t, theme] + ); + + return ( + + + + {t('federation-dashboard.gateways.label')} + + + + + + + ); +}; diff --git a/apps/guardian-ui/src/components/GuardiansCard.tsx b/apps/guardian-ui/src/components/GuardiansCard.tsx new file mode 100644 index 000000000..6f63f9f55 --- /dev/null +++ b/apps/guardian-ui/src/components/GuardiansCard.tsx @@ -0,0 +1,95 @@ +import { Card, CardBody, CardHeader, Text } from '@chakra-ui/react'; +import { StatusIndicator, Table, TableColumn, TableRow } from '@fedimint/ui'; +import { useTranslation } from '@fedimint/utils'; +import React, { useMemo } from 'react'; +import { ConfigResponse, PeerConnectionStatus, StatusResponse } from '../types'; + +type TableKey = 'name' | 'status' | 'health' | 'lastContribution'; + +interface Props { + status: StatusResponse | undefined; + config: ConfigResponse | undefined; +} + +export const GuardiansCard: React.FC = ({ status, config }) => { + const { t } = useTranslation(); + + const columns: TableColumn[] = useMemo( + () => [ + { + key: 'name', + heading: t('federation-dashboard.guardians.name-label'), + }, + { + key: 'status', + heading: t('federation-dashboard.guardians.name-label'), + }, + { + key: 'health', + heading: t('federation-dashboard.guardians.health-label'), + }, + { + key: 'lastContribution', + heading: t('federation-dashboard.guardians.last-contribution-label'), + }, + ], + [t] + ); + + const rows: TableRow[] = useMemo(() => { + if (!status?.federation || !config) return []; + const peerDataArray = []; + for (const [id, federationStatus] of Object.entries( + status.federation.status_by_peer + )) { + const numericId = parseInt(id, 10); + const endpoint = config.client_config.api_endpoints[numericId]; + if (endpoint) { + peerDataArray.push({ + key: id, + name: endpoint.name, + status: ( + + {federationStatus.connection_status} + + ), + health: ( + + {t( + federationStatus.flagged + ? 'federation-dashboard.guardians.health-issue' + : 'federation-dashboard.guardians.health-good' + )} + + ), + lastContribution: federationStatus.last_contribution + ? federationStatus.last_contribution + : t('common.unknown'), + }); + } + } + return peerDataArray; + }, [status, config, t]); + + return ( + + + + {t('federation-dashboard.guardians.label')} + + + +
+ + + ); +}; diff --git a/apps/guardian-ui/src/components/Pill.tsx b/apps/guardian-ui/src/components/Pill.tsx deleted file mode 100644 index e51b2c2b8..000000000 --- a/apps/guardian-ui/src/components/Pill.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { FC } from 'react'; -import { Flex, Box, Text, useColorModeValue, useTheme } from '@chakra-ui/react'; - -interface PillProp { - text: string; - status: string; - type?: 'error' | 'warning' | 'success'; -} - -// Have to update pill color based on quorum. -export const Pill: FC = ({ text, status, type }) => { - const theme = useTheme(); - const color = theme.colors[type || 'gray']; - const labelColor = useColorModeValue( - theme.colors.gray[500], - theme.colors.gray[400] - ); - const bgColor = useColorModeValue(type ? color[50] : color[100], color[700]); - const textColor = useColorModeValue(color[700], color[200]); - const dotColor = useColorModeValue(color[500], color[400]); - return ( - - - {text} - - - - - {status} - - - - ); -}; diff --git a/apps/guardian-ui/src/languages/en.json b/apps/guardian-ui/src/languages/en.json index 3965cf772..cd6ffb8f3 100644 --- a/apps/guardian-ui/src/languages/en.json +++ b/apps/guardian-ui/src/languages/en.json @@ -1,8 +1,10 @@ { "common": { + "bitcoin": "Bitcoin", "next": "Next", "back": "Back", - "error": "Something went wrong." + "error": "Something went wrong.", + "unknown": "Unknown" }, "connect-guardians": { "invite-guardians": "Invite Followers", @@ -15,21 +17,38 @@ "table-description": "Guardians will be confirmed here once they confirm Federation settings." }, "federation-dashboard": { - "placeholder-fed-description": "A community-based coalition focused on ushering the world into a bitcoin standard", "invite-members": "Invite members", "invite-members-prompt": "Share this to invite members to join the federation", "fed-info": { - "label": "Federation info", - "your-status-label": "Your status", + "label": "Federation Info", + "your-status-label": "Status", "epoch-count-label": "Epoch count", - "api-version-label": "API Version", + "api-version-label": "API version", "consensus-version-label": "Consensus version" }, - "peer-info": { - "label": "Peer info", - "id-label": "Id", + "balance": { + "label": "Balance" + }, + "bitcoin-node": { + "label": "Bitcoin Node", + "url-label": "URL", + "block-height-label": "Current block height" + }, + "guardians": { + "label": "Guardians", "name-label": "Name", - "status-label": "Status" + "status-label": "Connection Status", + "health-label": "Health", + "health-issue": "Issue", + "health-good": "Good", + "last-contribution-label": "Last contribution epoch" + }, + "gateways": { + "label": "Lightning Gateways", + "node-id-label": "Node ID", + "gateway-id-label": "Gateway ID", + "fee-label": "Gateway fee", + "view-on-site": "View on {{site}}" } }, "login": { diff --git a/packages/ui/src/CopyInput.tsx b/packages/ui/src/CopyInput.tsx index fb85c9cc9..17f9a5ffe 100644 --- a/packages/ui/src/CopyInput.tsx +++ b/packages/ui/src/CopyInput.tsx @@ -6,11 +6,12 @@ import { InputRightElement, useTheme, useClipboard, + Flex, } from '@chakra-ui/react'; export interface CopyInputProps { value: string; - size?: 'md' | 'lg'; + size?: 'sm' | 'md' | 'lg'; buttonLeftIcon?: React.ReactElement; } @@ -21,26 +22,48 @@ export const CopyInput: React.FC = ({ }) => { const { onCopy, hasCopied } = useClipboard(value); const theme = useTheme(); + const buttonWidth = { + lg: '115px', + md: '100px', + sm: '96px', + }[size]; + // Height is input - 2px to account for borders + const buttonHeight = theme.components.Input.sizes[size].height - 2; return ( - + diff --git a/packages/ui/src/KeyValues.tsx b/packages/ui/src/KeyValues.tsx new file mode 100644 index 000000000..e3e604f7d --- /dev/null +++ b/packages/ui/src/KeyValues.tsx @@ -0,0 +1,43 @@ +import { Flex, Text, useTheme } from '@chakra-ui/react'; +import React from 'react'; + +interface KeyValue { + key: string; + label?: React.ReactNode; + value: React.ReactNode; +} + +export interface KeyValuesProps { + keyValues: KeyValue[]; + direction?: 'row' | 'column'; +} + +export const KeyValues: React.FC = ({ + keyValues, + direction = 'column', +}) => { + const theme = useTheme(); + return ( + + {keyValues.map(({ key, label, value }) => ( + + + {label || key} + + + {value} + + + ))} + + ); +}; diff --git a/packages/ui/src/StatusIndicator.tsx b/packages/ui/src/StatusIndicator.tsx new file mode 100644 index 000000000..f3fe2ff52 --- /dev/null +++ b/packages/ui/src/StatusIndicator.tsx @@ -0,0 +1,43 @@ +import React, { FC } from 'react'; +import { Box, Text, useColorModeValue, useTheme } from '@chakra-ui/react'; + +interface StatusIndicatorProps { + status?: 'error' | 'warning' | 'success'; + children: React.ReactNode; +} + +// Have to update pill color based on quorum. +export const StatusIndicator: FC = ({ + status, + children, +}) => { + const theme = useTheme(); + const color = theme.colors[status || 'gray']; + const bgColor = useColorModeValue( + status ? color[50] : color[100], + color[700] + ); + const textColor = useColorModeValue(color[700], color[200]); + const dotColor = useColorModeValue(color[500], color[400]); + return ( + + + + {children} + + + ); +}; diff --git a/packages/ui/src/Table.tsx b/packages/ui/src/Table.tsx index 0e3ea8e76..8f0d14e67 100644 --- a/packages/ui/src/Table.tsx +++ b/packages/ui/src/Table.tsx @@ -45,6 +45,7 @@ export function Table({ boxShadow={theme.shadows.sm} borderRadius={12} width='100%' + overflow='hidden' > {hasHeading && ( ({ )} - + - {columns.map((column) => ( + {columns.map((column, idx) => ( diff --git a/packages/ui/src/Wrapper.tsx b/packages/ui/src/Wrapper.tsx index babd73e0a..73d80cc7d 100644 --- a/packages/ui/src/Wrapper.tsx +++ b/packages/ui/src/Wrapper.tsx @@ -5,10 +5,12 @@ import { Footer } from './Footer'; export interface WrapperProps { children: React.ReactNode; + size?: 'md' | 'lg'; } export const Wrapper = memo(function Wrapper({ children, + size = 'md', }: WrapperProps): JSX.Element { return ( { const css = { height: buttonSizes[size].height, px: Math.floor(buttonSizes[size].px * 0.75), + borderRadius: '8px', }; prev = { ...prev, @@ -282,6 +283,9 @@ export const theme = extendTheme( _hover: { borderColor: colors.border.hover, }, + _readOnly: { + color: colors.gray[500], + }, }, }, filled: { @@ -342,6 +346,26 @@ export const theme = extendTheme( }, }, }, + Card: { + baseStyle: { + container: { + boxShadow: shadows.sm, + borderBottom: `20px solid ${colors.gray[50]}`, + }, + }, + sizes: { + md: { + header: { + padding: '24px', + paddingBottom: '8px', + }, + body: { + padding: '24px', + paddingTop: '8px', + }, + }, + }, + }, }, }, // By default all components use blue color scheme diff --git a/packages/utils/src/format.tsx b/packages/utils/src/format.tsx new file mode 100644 index 000000000..45688fb48 --- /dev/null +++ b/packages/utils/src/format.tsx @@ -0,0 +1,11 @@ +/** + * Given a string, turn it into an "ellipsis sandwich" with the start and + * end showing. If the text is shorter than it would be with an ellipsis, + * the full string is returned instead. + */ +export function formatEllipsized(text: string, size = 6) { + if (text.length <= size * 2 + 3) { + return text; + } + return `${text.substring(0, size)}...${text.substring(text.length - size)}`; +} diff --git a/packages/utils/src/index.tsx b/packages/utils/src/index.tsx index e82230f1b..923e52b9d 100644 --- a/packages/utils/src/index.tsx +++ b/packages/utils/src/index.tsx @@ -1 +1,2 @@ export * from './i18n'; +export * from './format';
{column.heading}