adding token modals for admin config

This commit is contained in:
Roland Osborne 2025-02-21 15:22:05 -08:00
parent c4a6796740
commit 4d1a8d1516
4 changed files with 204 additions and 11 deletions

View File

@ -79,3 +79,34 @@
}
}
}
.modal {
.prompt {
padding: 8px;
font-size: 14px;
}
.copy {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 16px;
}
.value {
font-size: 12px;
padding: 16px;
}
.icon {
cursor: pointer;
color: var(--mantine-color-green-2);
}
.control {
width: 100%;
display: flex;
justify-content: flex-end;
}
}

View File

@ -1,20 +1,115 @@
import { useEffect, useState } from 'react';
import classes from './Accounts.module.css'
import { useAccounts } from './useAccounts.hook'
import { Modal, Divider, Text, ActionIcon } from '@mantine/core'
import { IconUserPlus, IconUserCheck, IconReload, IconSettings, IconLockOpen2, IconUserCancel, IconTrash } from '@tabler/icons-react'
import { Modal, Divider, Text, ActionIcon, Button } from '@mantine/core'
import { IconUserPlus, IconUserCheck, IconCopy, IconCheck, IconReload, IconSettings, IconLockOpen2, IconUserCancel, IconTrash } from '@tabler/icons-react'
import { Card } from '../card/Card'
import { Colors } from '../constants/Colors';
import { modals } from '@mantine/modals'
import { useDisclosure } from '@mantine/hooks'
export function Accounts({ openSetup }: { openSetup: ()=>void }) {
const { state, actions } = useAccounts();
const [failed, setFailed] = useState(false);
const [loading, setLoading] = useState(false);
const [blocking, setBlocking] = useState(null as null | number);
const [removing, setRemoving] = useState(null as null | number);
const [accessing, setAccessing] = useState(null as null | number);
const [adding, setAdding] = useState(false);
const [accessOpened, { open: accessOpen, close: accessClose }] = useDisclosure(false)
const [addOpened, { open: addOpen, close: addClose }] = useDisclosure(false)
const [tokenCopy, setTokenCopy] = useState(false);
const [linkCopy, setLinkCopy] = useState(false);
const [token, setToken] = useState('');
const link = `${window.location.origin}/#/create?add=${token}`;
useEffect(() => {
actions.reload();
}, []);
const loadAccounts = async () => {
if (!loading) {
setLoading(true);
try {
await actions.reload();
} catch (err) {
console.log(err);
}
setLoading(false);
}
}
const copyToken = async () => {
if (!tokenCopy) {
try {
navigator.clipboard.writeText(token)
setTokenCopy(true);
setTimeout(() => {
setTokenCopy(false);
}, 2000);
} catch (err) {
console.log(err);
}
}
};
const copyLink = async () => {
if (!linkCopy) {
try {
navigator.clipboard.writeText(link)
setLinkCopy(true);
setTimeout(() => {
setLinkCopy(false);
}, 2000);
} catch (err) {
console.log(err);
}
}
};
const showError = () => {
modals.openConfirmModal({
title: state.strings.operationFailed,
withCloseButton: true,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: <Text>{state.strings.tryAgain}</Text>,
cancelProps: { display: 'none' },
confirmProps: { display: 'none' },
})
}
const addAccount = async () => {
if (!adding) {
setAdding(true);
try {
const access = await actions.addAccount();
setToken(access);
addOpen();
} catch (err) {
console.log(err);
showError();
}
setAdding(false);
}
}
const accessAccount = async (accountId: number) => {
if (!accessing) {
setAccessing(accountId);
try {
const access = await actions.accessAccount(accountId);
setToken(access);
accessOpen();
} catch (err) {
console.log(err);
showError();
}
setAccessing(null);
}
}
const blockAccount = async (accountId: number, block: boolean) => {
if (!blocking) {
setBlocking(accountId);
@ -22,15 +117,42 @@ export function Accounts({ openSetup }: { openSetup: ()=>void }) {
await actions.blockAccount(accountId, block);
} catch (err) {
console.log(err);
setFailed(true);
showError();
}
setBlocking(null);
}
}
const removeAccount = (accountId: number) => {
modals.openConfirmModal({
title: state.strings.confirmDelete,
withCloseButton: false,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: <Text>{ state.strings.areSure }</Text>,
labels: { confirm: state.strings.remove, cancel: state.strings.cancel },
onConfirm: async () => {
if (!removing) {
setRemoving(accountId);
try {
await actions.removeAccount(accountId);
} catch (err) {
console.log(err);
showError();
}
setRemoving(null);
}
}
});
}
const members = state.members.map((member, idx) => {
const options = [
<ActionIcon key="acess" className={classes.action} variant="light" onClick={actions.reload} loading={false}><IconLockOpen2 /></ActionIcon>,
<ActionIcon key="acess" className={classes.action} variant="light" loading={removing === member.accountId} onClick={() => accessAccount(member.accountId)}>
<IconLockOpen2 />
</ActionIcon>,
<ActionIcon key="block" className={classes.action} variant="light" loading={blocking === member.accountId} color={Colors.pending} onClick={() => blockAccount(member.accountId, !member.disabled)}>
{ member.disabled && (
<IconUserCheck />
@ -39,7 +161,7 @@ export function Accounts({ openSetup }: { openSetup: ()=>void }) {
<IconUserCancel />
)}
</ActionIcon>,
<ActionIcon key="remove" className={classes.action} variant="light" onClick={actions.reload} loading={false} color={Colors.offsync}><IconTrash /></ActionIcon>,
<ActionIcon key="remove" className={classes.action} variant="light" loading={removing === member.accountId} color={Colors.offsync} onClick={() => removeAccount(member.accountId)}><IconTrash /></ActionIcon>,
];
return (
@ -52,7 +174,7 @@ export function Accounts({ openSetup }: { openSetup: ()=>void }) {
<div className={classes.content}>
<div className={classes.header}>
{ state.layout !== 'large' && (
<ActionIcon className={classes.action} variant="light" onClick={actions.reload} loading={state.loading}>
<ActionIcon className={classes.action} variant="light" onClick={actions.reload} loading={loading}>
<IconReload />
</ActionIcon>
)}
@ -64,7 +186,7 @@ export function Accounts({ openSetup }: { openSetup: ()=>void }) {
<IconReload />
</ActionIcon>
)}
<ActionIcon className={classes.action} variant="light" onClick={()=>{}}>
<ActionIcon className={classes.action} variant="light" onClick={addAccount}>
<IconUserPlus />
</ActionIcon>
{ state.layout === 'large' && (
@ -77,6 +199,44 @@ export function Accounts({ openSetup }: { openSetup: ()=>void }) {
{ members }
</div>
</div>
<Modal title={state.strings.addingTitle} size="lg" opened={addOpened} onClose={addClose} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} centered>
<div className={classes.modal}>
<Text className={classes.prompt}>{state.strings.addingLink}:</Text>
<div className={classes.copy}>
<Text className={classes.value}>{ link }</Text>
{linkCopy && <IconCheck size="16" />}
{!linkCopy && <IconCopy size="16" className={classes.icon} onClick={copyLink} />}
</div>
<Text className={classes.prompt}>{state.strings.addingToken}:</Text>
<div className={classes.copy}>
<Text className={classes.value}>{ token }</Text>
{tokenCopy && <IconCheck size="16" />}
{!tokenCopy && <IconCopy size="16" className={classes.icon} onClick={copyToken} />}
</div>
<div className={classes.control}>
<Button onClick={addClose}>{ state.strings.close }</Button>
</div>
</div>
</Modal>
<Modal title={state.strings.accessingTitle} size="lg" opened={accessOpened} onClose={accessClose} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} centered>
<div className={classes.modal}>
<Text className={classes.prompt}>{state.strings.accessingLink}:</Text>
<div className={classes.copy}>
<Text className={classes.value}>{ link }</Text>
{linkCopy && <IconCheck size="16" />}
{!linkCopy && <IconCopy size="16" className={classes.icon} onClick={copyLink} />}
</div>
<Text className={classes.prompt}>{state.strings.accessingToken}:</Text>
<div className={classes.copy}>
<Text className={classes.value}>{ token }</Text>
{tokenCopy && <IconCheck size="16" />}
{!tokenCopy && <IconCopy size="16" className={classes.icon} onClick={copyToken} />}
</div>
<div className={classes.control}>
<Button onClick={addClose}>{ state.strings.close }</Button>
</div>
</div>
</Modal>
</div>
);
}

View File

@ -71,8 +71,8 @@ export function Setup() {
<Text className={classes.label}>{state.strings.keyType}:</Text>
<Radio.Group name="keyType" className={classes.radio} value={state.setup?.keyType} onChange={actions.setKeyType}>
<Group mt="xs">
<Radio disabled={state.loading} value="RSA_2048" label="RSA2048" />
<Radio disabled={state.loading} value="RSA_4096" label="RSA4096" />
<Radio disabled={state.loading} value="RSA2048" label="RSA2048" />
<Radio disabled={state.loading} value="RSA4096" label="RSA4096" />
</Group>
</Radio.Group>
</div>

View File

@ -153,7 +153,7 @@ export function useSetup() {
}
},
setKeyType: (type: string) => {
const keyType = type === 'RSA_2048' ? KeyType.RSA_2048 : KeyType.RSA_4096;
const keyType = type === 'RSA2048' ? KeyType.RSA_2048 : KeyType.RSA_4096;
if (setup.current) {
setup.current.keyType = keyType;
updateState({ setup: setup.current });
@ -231,6 +231,8 @@ export function useSetup() {
},
setEnableService: (iceService: boolean) => {
if (setup.current) {
const iceUrl = iceService ? 'https://rtc.live.cloudflare.com/v1/turn/keys/%%TURN_KEY_ID%%/credentials/generate' : '';
setup.current.iceUrl = iceUrl;
setup.current.iceService = iceService ? ICEService.Cloudflare : ICEService.Default;
updateState({ setup: setup.current });
save();