Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: homepage actions and federation selector #527

Merged
merged 11 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
31 changes: 31 additions & 0 deletions apps/router/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { Flex } from '@chakra-ui/react';
import { Logo } from './Logo';
import { ServiceMenu } from './ServiceMenu';
import { useActiveService } from '../hooks';
import { useAppContext } from '../context/hooks';

export const Header = React.memo(function Header() {
const { guardians, gateways } = useAppContext();
const { activeServiceId } = useActiveService();

const hasServices =
Object.keys(guardians).length > 0 || Object.keys(gateways).length > 0;

return (
<Flex
width='100%'
justifyContent={['center', 'space-between']}
mb={['24px', '12px']}
>
<Logo />
{hasServices && (
<ServiceMenu
guardians={guardians}
gateways={gateways}
activeServiceId={activeServiceId}
/>
)}
</Flex>
);
});
42 changes: 42 additions & 0 deletions apps/router/src/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Flex } from '@chakra-ui/react';

export const Logo = () => (
<Flex alignItems='center' justifyContent={['center', 'flex-start']} gap='2'>
<a href='/'>
<svg
width='200'
height='46'
viewBox='0 0 200 46'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M26.8363 3.68341C26.8363 5.71771 25.1872 7.36683 23.1529 7.36683C21.1186 7.36683 19.4695 5.71771 19.4695 3.68341C19.4695 1.64912 21.1186 0 23.1529 0C25.1872 0 26.8363 1.64912 26.8363 3.68341ZM42.4491 11.205C42.4491 13.2393 40.8 14.8885 38.7657 14.8885C36.7314 14.8885 35.0822 13.2393 35.0822 11.205C35.0822 9.17074 36.7314 7.52163 38.7657 7.52163C40.8 7.52163 42.4491 9.17074 42.4491 11.205ZM42.6224 31.7826C44.6567 31.7826 46.3058 30.1335 46.3058 28.0992C46.3058 26.0649 44.6567 24.4158 42.6224 24.4158C40.5881 24.4158 38.939 26.0649 38.939 28.0992C38.939 30.1335 40.5881 31.7826 42.6224 31.7826ZM35.5001 41.6474C35.5001 43.6817 33.851 45.3308 31.8167 45.3308C29.7824 45.3308 28.1333 43.6817 28.1333 41.6474C28.1333 39.6131 29.7824 37.964 31.8167 37.964C33.851 37.964 35.5001 39.6131 35.5001 41.6474ZM14.4892 45.3308C16.5234 45.3308 18.1726 43.6817 18.1726 41.6474C18.1726 39.6131 16.5234 37.964 14.4892 37.964C12.4549 37.964 10.8057 39.6131 10.8057 41.6474C10.8057 43.6817 12.4549 45.3308 14.4892 45.3308ZM7.36683 28.0992C7.36683 30.1335 5.71771 31.7826 3.68342 31.7826C1.64912 31.7826 0 30.1335 0 28.0992C0 26.0649 1.64912 24.4158 3.68342 24.4158C5.71771 24.4158 7.36683 26.0649 7.36683 28.0992ZM7.54018 14.8885C9.57447 14.8885 11.2236 13.2393 11.2236 11.205C11.2236 9.17074 9.57447 7.52163 7.54018 7.52163C5.50588 7.52163 3.85676 9.17074 3.85676 11.205C3.85676 13.2393 5.50588 14.8885 7.54018 14.8885ZM29.6066 18.5099L23.1529 15.4022L16.6992 18.5099L15.1051 25.4929L19.5717 31.0923H26.7342L31.2007 25.4929L29.6066 18.5099ZM23.1498 10.9372L23.1529 10.9357H23.1467L23.1498 10.9372ZM13.2077 15.7241L23.1498 10.9372L33.0919 15.7241L35.5496 26.4865L28.6688 35.1162H17.6309L10.7501 26.4865L13.2077 15.7241Z'
fill='url(#paint0_radial_5520_78248)'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M160.244 14.7112C161.676 14.7112 162.836 13.551 162.836 12.1198C162.836 10.6886 161.676 9.52832 160.244 9.52832C158.813 9.52832 157.653 10.6886 157.653 12.1198C157.653 13.551 158.813 14.7112 160.244 14.7112ZM57.6797 9.9012V35.2975H61.598V24.3408H72.4095V20.7853H61.598V13.4567H73.3528V9.9012H57.6797ZM79.819 34.6808C81.1734 35.4306 82.7577 35.8055 84.5717 35.8055C86.1922 35.8055 87.5225 35.5515 88.5625 35.0436C89.6026 34.5356 90.437 33.9068 91.0659 33.157C91.7189 32.4072 92.2148 31.6937 92.5534 31.0164L89.4695 29.4201C89.0826 30.2425 88.5263 30.9681 87.8006 31.5969C87.0992 32.2258 86.0471 32.5402 84.6443 32.5402C83.1447 32.5402 81.8869 32.0807 80.8711 31.1616C79.8794 30.2183 79.3594 28.9605 79.311 27.3884H93.0613V25.9734C93.0613 24.1594 92.6985 22.5752 91.9729 21.2207C91.2473 19.8662 90.2314 18.8141 88.9253 18.0643C87.6434 17.2903 86.1438 16.9033 84.4266 16.9033C82.6609 16.9033 81.1009 17.2903 79.7464 18.0643C78.4161 18.8141 77.3761 19.8904 76.6263 21.2933C75.8765 22.6719 75.5016 24.2925 75.5016 26.1549V26.5902C75.5016 28.4284 75.8765 30.049 76.6263 31.4518C77.4003 32.8305 78.4645 33.9068 79.819 34.6808ZM89.2519 24.5222H79.3473C79.5408 23.1678 80.0729 22.1156 80.9437 21.3658C81.8386 20.5918 82.9874 20.2049 84.3903 20.2049C85.7931 20.2049 86.9299 20.5918 87.8006 21.3658C88.6714 22.1156 89.1551 23.1678 89.2519 24.5222ZM104.235 35.8055C102.76 35.8055 101.393 35.4427 100.135 34.717C98.8777 33.9914 97.8619 32.9514 97.0879 31.5969C96.3381 30.2183 95.9632 28.5615 95.9632 26.6265V26.0823C95.9632 24.1715 96.3381 22.5268 97.0879 21.1481C97.8377 19.7695 98.8414 18.7174 100.099 17.9917C101.357 17.2661 102.736 16.9033 104.235 16.9033C105.396 16.9033 106.364 17.0485 107.138 17.3387C107.936 17.6289 108.589 18.0038 109.097 18.4634C109.605 18.8988 109.992 19.3583 110.258 19.842H110.838V9.9012H114.575V35.2975H110.911V32.7579H110.33C109.871 33.5319 109.169 34.2333 108.226 34.8622C107.307 35.491 105.977 35.8055 104.235 35.8055ZM105.287 32.5402C106.908 32.5402 108.238 32.0202 109.278 30.9802C110.342 29.9401 110.874 28.4526 110.874 26.5177V26.1911C110.874 24.2804 110.354 22.805 109.314 21.7649C108.274 20.7249 106.932 20.2049 105.287 20.2049C103.691 20.2049 102.361 20.7249 101.296 21.7649C100.256 22.805 99.7363 24.2804 99.7363 26.1911V26.5177C99.7363 28.4526 100.256 29.9401 101.296 30.9802C102.361 32.0202 103.691 32.5402 105.287 32.5402ZM127.908 17.4113V35.2975H131.644V24.6311C131.644 23.2282 131.995 22.1519 132.697 21.4021C133.398 20.6523 134.329 20.2774 135.49 20.2774C136.579 20.2774 137.413 20.5798 137.993 21.1844C138.598 21.7649 138.9 22.5994 138.9 23.6878V35.2975H142.637V24.6311C142.637 23.2282 142.988 22.1519 143.689 21.4021C144.391 20.6523 145.322 20.2774 146.483 20.2774C147.571 20.2774 148.406 20.5798 148.986 21.1844C149.591 21.7649 149.893 22.5994 149.893 23.6878V35.2975H153.63V23.3975C153.63 22.0189 153.364 20.87 152.832 19.9509C152.3 19.0076 151.587 18.3062 150.692 17.8466C149.797 17.3629 148.769 17.121 147.608 17.121C146.157 17.121 145.032 17.3992 144.234 17.9555C143.46 18.5118 142.867 19.2011 142.456 20.0235H141.875C141.464 19.1769 140.848 18.4876 140.025 17.9555C139.227 17.3992 138.163 17.121 136.832 17.121C135.551 17.121 134.535 17.3629 133.785 17.8466C133.035 18.3304 132.491 18.8867 132.152 19.5155H131.572V17.4113H127.908ZM166.847 35.2975V17.4113H170.511V20.096H171.091C171.43 19.3704 172.035 18.6932 172.905 18.0643C173.776 17.4354 175.07 17.121 176.787 17.121C178.142 17.121 179.339 17.4234 180.379 18.028C181.443 18.6327 182.278 19.4913 182.883 20.6039C183.487 21.6924 183.79 23.0105 183.79 24.5585V35.2975H180.053V24.8488C180.053 23.3008 179.666 22.164 178.892 21.4384C178.118 20.6886 177.053 20.3137 175.699 20.3137C174.151 20.3137 172.905 20.8216 171.962 21.8375C171.043 22.8533 170.583 24.3287 170.583 26.2637V35.2975H166.847ZM191.873 34.318C192.526 34.971 193.397 35.2975 194.485 35.2975H199.419V32.1411H195.682C195.005 32.1411 194.666 31.7783 194.666 31.0527V20.5677H200V17.4113H194.666V11.4975H190.93V17.4113H185.995V20.5677H190.93V31.7058C190.93 32.7942 191.244 33.6649 191.873 34.318ZM158.344 35.2975V17.4113H162.081V35.2975H158.344ZM123.791 12.1198C123.791 13.551 122.631 14.7113 121.2 14.7113C119.769 14.7113 118.608 13.551 118.608 12.1198C118.608 10.6886 119.769 9.52833 121.2 9.52833C122.631 9.52833 123.791 10.6886 123.791 12.1198ZM119.299 35.2976V17.4113H123.036V35.2976H119.299Z'
fill='#170022'
/>
<defs>
<radialGradient
id='paint0_radial_5520_78248'
cx='0'
cy='0'
r='1'
gradientUnits='userSpaceOnUse'
gradientTransform='translate(23.1529 23.8814) rotate(180) scale(25.7909 25.7909)'
>
<stop offset='0.507956' stopColor='#4AD6FF' />
<stop offset='1' stopColor='#181884' />
</radialGradient>
</defs>
</svg>
</a>
</Flex>
);
87 changes: 87 additions & 0 deletions apps/router/src/components/ServiceMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState } from 'react';
import {
Menu,
MenuButton,
MenuItem,
MenuList,
Button,
Text,
} from '@chakra-ui/react';
import { FiMenu } from 'react-icons/fi';
import { useNavigate } from 'react-router-dom';
import { ServiceType, ServiceConfig } from '../types';

type ServiceMenuProps = {
guardians: Record<string, ServiceConfig>;
gateways: Record<string, ServiceConfig>;
activeServiceId: string | null;
};

export const ServiceMenu: React.FC<ServiceMenuProps> = ({
guardians,
gateways,
activeServiceId,
}) => {
const [isOpen, setIsOpen] = useState(false);
const navigate = useNavigate();

const handleServiceSelect = (type: ServiceType, id: string) => {
navigate(`/${type}/${id}`);
setIsOpen(false);
};

const renderServiceList = (
services: Record<string, ServiceConfig>,
type: ServiceType
) => (
<>
<MenuItem
as='div'
fontWeight='bold'
_hover={{ background: 'none' }}
cursor='default'
>
{type === 'guardian' ? 'Guardians' : 'Gateways'}
</MenuItem>
{Object.entries(services).map(([id, service]) => (
<MenuItem
key={id}
onClick={() =>
`${type}/${id}` !== activeServiceId && handleServiceSelect(type, id)
}
pl={4}
bg={`${type}/${id}` === activeServiceId ? 'blue.50' : 'transparent'}
_hover={{
bg: `${type}/${id}` === activeServiceId ? 'blue.50' : 'gray.100',
}}
cursor={`${type}/${id}` === activeServiceId ? 'default' : 'pointer'}
>
<Text fontSize='sm'>{service.config.baseUrl}</Text>
</MenuItem>
))}
</>
);

return (
<Menu
isOpen={isOpen}
onClose={() => setIsOpen(false)}
placement='bottom-end'
closeOnBlur={true}
>
<MenuButton
as={Button}
leftIcon={<FiMenu />}
size='md'
variant='outline'
onClick={() => setIsOpen(!isOpen)}
/>
<MenuList>
{Object.keys(guardians).length > 0 &&
renderServiceList(guardians, 'guardian')}
{Object.keys(gateways).length > 0 &&
renderServiceList(gateways, 'gateway')}
</MenuList>
</Menu>
);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React, { memo } from 'react';
import { Box, Flex, useBreakpointValue } from '@chakra-ui/react';
import { Header, HeaderProps } from './Header';
import { Header } from './Header';
import { Footer } from './Footer';

export interface WrapperProps {
headerProps?: HeaderProps;
children: React.ReactNode;
}

export const Wrapper = memo(function Wrapper({
headerProps,
children,
}: WrapperProps): JSX.Element {
const size = useBreakpointValue({ base: 'md', lg: 'lg' });
Expand All @@ -30,7 +28,7 @@ export const Wrapper = memo(function Wrapper({
width='100%'
alignItems='center'
>
<Header {...headerProps} />
<Header />
{children}
</Box>
<Footer />
Expand Down
32 changes: 32 additions & 0 deletions apps/router/src/context/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export enum APP_ACTION_TYPE {
ADD_GATEWAY = 'ADD_GATEWAY',
REMOVE_GUARDIAN = 'REMOVE_GUARDIAN',
REMOVE_GATEWAY = 'REMOVE_GATEWAY',
UPDATE_GUARDIAN = 'UPDATE_GUARDIAN',
UPDATE_GATEWAY = 'UPDATE_GATEWAY',
}

export type AppAction =
Expand All @@ -76,6 +78,20 @@ export type AppAction =
| {
type: APP_ACTION_TYPE.REMOVE_GATEWAY;
payload: string;
}
| {
type: APP_ACTION_TYPE.UPDATE_GUARDIAN;
payload: {
id: string;
guardian: Guardian;
};
}
| {
type: APP_ACTION_TYPE.UPDATE_GATEWAY;
payload: {
id: string;
gateway: Gateway;
};
};

const saveToLocalStorage = (state: AppContextValue) => {
Expand Down Expand Up @@ -121,6 +137,22 @@ const reducer = (
)
),
};
case APP_ACTION_TYPE.UPDATE_GUARDIAN:
return {
...state,
guardians: {
...state.guardians,
[action.payload.id]: action.payload.guardian,
},
};
case APP_ACTION_TYPE.UPDATE_GATEWAY:
return {
...state,
gateways: {
...state.gateways,
[action.payload.id]: action.payload.gateway,
},
};
}
};

Expand Down
85 changes: 46 additions & 39 deletions apps/router/src/home/ConnectServiceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState } from 'react';
import {
Button,
FormControl,
FormHelperText,
FormLabel,
Input,
Modal,
Expand All @@ -10,12 +11,12 @@ import {
ModalContent,
ModalHeader,
ModalOverlay,
useToast,
} from '@chakra-ui/react';
import { sha256Hash, useTranslation } from '@fedimint/utils';
import { GuardianConfig } from '../guardian-ui/GuardianApi';
import { useAppContext } from '../context/hooks';
import { APP_ACTION_TYPE } from '../context/AppContext';
import { GatewayConfig } from '../gateway-ui/types';
import { checkServiceExists, getServiceType } from './utils';

interface ConnectServiceModalProps {
isOpen: boolean;
Expand All @@ -30,65 +31,71 @@ export const ConnectServiceModal: React.FC<ConnectServiceModalProps> = ({
const [configUrl, setConfigUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { guardians, gateways, dispatch } = useAppContext();
const toast = useToast();

const handleSubmit = async () => {
setIsLoading(true);
const isWebSocket =
configUrl.startsWith('ws://') || configUrl.startsWith('wss://');
const isHttp =
configUrl.startsWith('http://') || configUrl.startsWith('https://');
try {
if (checkServiceExists(configUrl, guardians, gateways)) {
throw new Error('A service with this URL already exists');
}

const configUrlHash = await sha256Hash(configUrl);
const serviceType = getServiceType(configUrl);
const id = await sha256Hash(configUrl);

if (isWebSocket) {
if (Object.hasOwn(guardians, configUrlHash)) {
console.error('Guardian already exists');
return;
if (serviceType === 'guardian') {
dispatch({
type: APP_ACTION_TYPE.ADD_GUARDIAN,
payload: { id, guardian: { config: { baseUrl: configUrl } } },
});
} else {
dispatch({
type: APP_ACTION_TYPE.ADD_GATEWAY,
payload: { id, gateway: { config: { baseUrl: configUrl } } },
});
}
const guardianConfig: GuardianConfig = { baseUrl: configUrl };
dispatch({
type: APP_ACTION_TYPE.ADD_GUARDIAN,
payload: {
id: configUrlHash,
guardian: { config: guardianConfig },
},

setConfigUrl('');
onClose();
toast({
title: 'Service added',
status: 'success',
duration: 3000,
isClosable: true,
});
} else if (isHttp) {
if (Object.hasOwn(gateways, configUrlHash)) {
console.error('Gateway already exists');
return;
}
const gatewayConfig: GatewayConfig = { baseUrl: configUrl };
dispatch({
type: APP_ACTION_TYPE.ADD_GATEWAY,
payload: {
id: configUrlHash,
gateway: { config: gatewayConfig },
},
} catch (error) {
toast({
title: 'Error adding service',
description:
error instanceof Error ? error.message : 'An unknown error occurred',
status: 'error',
duration: 5000,
isClosable: true,
});
} else {
console.error('Invalid URL format');
return;
} finally {
setIsLoading(false);
}
setIsLoading(false);
setConfigUrl('');
onClose();
};

return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('home.addService', 'Add Service')}</ModalHeader>
<ModalHeader>
{t('home.connect-service-modal.title', 'Connect a Service')}
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<FormControl>
<FormLabel>{t('notConfigured.urlLabel')}</FormLabel>
<FormLabel>{t('home.connect-service-modal.url-label')}</FormLabel>
<Input
placeholder='wss://fedimintd.my-awesome-domain.com:6000'
placeholder='wss://fedimintd.domain.com:6000'
value={configUrl}
onChange={(e) => setConfigUrl(e.target.value)}
/>
<FormHelperText fontSize='sm'>
{t('home.connect-service-modal.helper-text')}
</FormHelperText>
</FormControl>
<Button
mt={4}
Expand Down
Loading
Loading