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: Agent deletion #127

Merged
merged 4 commits into from
May 11, 2023
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
2 changes: 1 addition & 1 deletion packages/siera-desktop/src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const App = () => {
>
<UpdateInformationProvider checkUpdate={checkForUpdates}>
<ConfigProvider configRepository={configRepository}>
<AgentManagerProvider>
<AgentManagerProvider agentDependenciesProvider={agentDependenciesProvider}>
<AgentContextWrapper>
<SieraUiApp router={router} />
</AgentContextWrapper>
Expand Down
4 changes: 3 additions & 1 deletion packages/siera-ui/src/SieraUiApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { sieraUiTheme } from './SieraUiTheme'
import { GlobalErrorHandler } from './components/GlobalErrorHandler'
import { UpdateNotifierModal } from './components/UpdateNotifierModal'
import { useConfigUnsafe } from './contexts/ConfigProvider'
import { CreateAgentModal, PresentInviteModal, ConfirmActionModal } from './modals'
import { ConfirmActionModal } from './modals/ConfirmActionModal'
import { CreateAgentModal } from './modals/CreateAgentModal'
import { PresentInviteModal } from './modals/PresentInviteModal'

interface SieraUiAppProps {
router: RouterProviderProps['router']
Expand Down
5 changes: 5 additions & 0 deletions packages/siera-ui/src/SieraUiTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const sieraUiTheme = (colorScheme: ColorScheme): MantineThemeOverride =>
dropdown: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.backgroundOne[7] : '#ffffff',
},
item: {
padding: '0.4rem 0.6rem',
minWidth: '8rem',
},
}),
},

Expand All @@ -39,6 +43,7 @@ export const sieraUiTheme = (colorScheme: ColorScheme): MantineThemeOverride =>
fontWeight: 700,
fontSize: '1.7rem',
padding: `0 ${theme.spacing.xl}px`,
color: theme.colors.textOne[7],
},
modal: {
border: `1px solid ${theme.colors.backgroundOne[6]}`,
Expand Down
11 changes: 8 additions & 3 deletions packages/siera-ui/src/components/RecordActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ActionIcon, createStyles, Group, Loader } from '@mantine/core'
import { IconTrash } from '@tabler/icons'
import React from 'react'

import { openConfirmActionModal } from '../modals'
import { openConfirmActionModal } from '../modals/ConfirmActionModal'

import { PrimaryButton, SecondaryButton } from './generic'

Expand All @@ -28,9 +28,14 @@ export const RecordActions = ({ onAccept, onDecline, onDelete, isLoading, propag
returnFn()
}

const dangerAction = (returnFn: () => void) => (event: React.MouseEvent<HTMLButtonElement>) => {
const dangerAction = (confirmMethod: () => void) => (event: React.MouseEvent<HTMLButtonElement>) => {
!propagateEvent && event.stopPropagation()
openConfirmActionModal('Delete record', 'Are you sure you want to delete this record?', returnFn)
openConfirmActionModal({
title: 'Delete record',
description: 'Are you sure you want to delete this record?',
confirmLabel: 'Delete',
onConfirm: confirmMethod,
})
}

const actions = [
Expand Down
13 changes: 12 additions & 1 deletion packages/siera-ui/src/components/generic/buttons/Buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ButtonProps } from '@mantine/core'
import type { ButtonHTMLAttributes } from 'react'

import { Button, createStyles } from '@mantine/core'
import { useColorScheme } from '@mantine/hooks'
import { IconArrowLeft, IconPlus } from '@tabler/icons'
import React from 'react'

Expand Down Expand Up @@ -43,12 +44,22 @@ type PrimaryButtonProps = StandardButton & {
}

export const PrimaryButton = (props: PrimaryButtonProps) => {
const colorScheme = useColorScheme()
const { classes, cx } = useStyles()
const { withPlusIcon } = props

const plusButton = withPlusIcon && <IconPlus size={16} stroke={3} />

return <Button leftIcon={plusButton} {...props} className={cx(classes.primary, props.className)} />
return (
<Button
leftIcon={plusButton}
loaderProps={{
stroke: colorScheme === 'dark' ? 'black' : 'white',
}}
{...props}
className={cx(classes.primary, props.className)}
/>
)
}

export const SecondaryButton = (props: StandardButton) => {
Expand Down
41 changes: 38 additions & 3 deletions packages/siera-ui/src/contexts/AgentManagerContext.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type { AgentConfigRecord } from '@animo/siera-core'
import type { AgentConfigRecord, AgentDependenciesProvider } from '@animo/siera-core'
import type { ReactNode } from 'react'

import { agentInitializer } from '@animo/siera-core/src/agent/AgentInitializer'
import { showNotification } from '@mantine/notifications'
import React, { createContext, useContext, useMemo, useState } from 'react'

import { openConfirmActionModal } from '../modals/ConfirmActionModal'

import { useConfigUnsafe } from './ConfigProvider'

export interface IAgentContext {
agents: AgentConfigRecord[]
currentAgentId?: string
setCurrentAgentId: (id: string | undefined) => void
addAgent: (agent: AgentConfigRecord) => Promise<void>
removeAgent: (agentId: string) => Promise<void>
loading: boolean
logout: () => void
}
Expand All @@ -30,23 +35,53 @@ export const useCurrentAgentRecord = (): AgentConfigRecord | undefined => {

interface AgentManagerProviderProps {
children?: ReactNode
agentDependenciesProvider: AgentDependenciesProvider
}

export const AgentManagerProvider = ({ children }: AgentManagerProviderProps) => {
const { config, addAgent, loading } = useConfigUnsafe()
export const AgentManagerProvider = ({ children, agentDependenciesProvider }: AgentManagerProviderProps) => {
const { config, addAgent, removeAgent: removeAgentFromConfig, loading } = useConfigUnsafe()
const [currentAgentId, setCurrentAgentId] = useState<string>()

const logout = () => {
setCurrentAgentId(undefined)
}

const removeAgent = async (agentId: string) => {
const agentRecord = config?.agents.find((agent) => agent.id === agentId)
if (!agentRecord) {
showNotification({
title: 'Error',
message: `Agent with id ${agentId} not found`,
})
return
}

try {
const loadedAgent = await agentInitializer(agentRecord.agentConfig, agentDependenciesProvider)
await loadedAgent.wallet.delete()
await loadedAgent.shutdown()
} catch (error) {
openConfirmActionModal({
title: 'Error while removing agent',
description: 'Do you want to continue? The agent wallet will not be deleted.',
onConfirm: async () => {
await removeAgentFromConfig(agentId)
},
})
return
}

await removeAgentFromConfig(agentId)
}

return (
<AgentManagerContext.Provider
value={{
setCurrentAgentId,
currentAgentId,
agents: config?.agents || [],
addAgent,
removeAgent,
loading,
logout,
}}
Expand Down
10 changes: 10 additions & 0 deletions packages/siera-ui/src/contexts/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type ConfigContext = {
config?: SieraUiConfig
loading: boolean
addAgent: (agent: AgentConfigRecord) => Promise<void>
removeAgent: (agentId: string) => Promise<void>
setColorScheme: (colorScheme: 'dark' | 'light') => Promise<void>
}

Expand Down Expand Up @@ -62,6 +63,14 @@ export const ConfigProvider = ({ children, configRepository }: ConfigProviderPro
setConfig(updatedConfig)
}

const removeAgent = async (agentId: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimoGlastra I assume AFJ gives some utility on the wallet to be deleted, right?

If so, we might have to instantiate the agent here to be deleted.

Or, we would just remove all associated files, which is fairly easy but we can do that when we move to 0.4.0 as it changed there and it would add some useless code for now.

Copy link
Member Author

@Tommylans Tommylans May 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I found the delete function on the wallet. I'm currently working on a solution to initialise the wallet for the deletion 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need to initialize the wallet, as long as it has the walletConfig. But I thin you need to initialize it to get the walletConfig in currently.

We're going to revamp the wallet interface for 0.5.0, and we'll make sure to make this easier (we're thinking about a separate wallet management API that allows to create, remove, open, etc.. wallets, that is separate from the actual wallet operations such as signing ...)

if (!config) return

const updatedConfig = { ...config, agents: config.agents.filter((a) => a.id !== agentId) }
await saveConfig(updatedConfig)
setConfig(updatedConfig)
}

const setColorScheme = async (colorScheme: 'dark' | 'light') => {
if (!config) return

Expand All @@ -76,6 +85,7 @@ export const ConfigProvider = ({ children, configRepository }: ConfigProviderPro
config,
loading,
addAgent,
removeAgent,
setColorScheme,
}}
>
Expand Down
6 changes: 3 additions & 3 deletions packages/siera-ui/src/layout/LayoutNavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Agent } from '@aries-framework/core'

import { ActionIcon, createStyles, Flex, Menu, Navbar, useMantineColorScheme } from '@mantine/core'
import { IconChevronDown } from '@tabler/icons'
import { IconDotsVertical } from '@tabler/icons'
import React, { useState } from 'react'

import { useAgentManager } from '../contexts/AgentManagerContext'
Expand Down Expand Up @@ -71,10 +71,10 @@ export const LayoutNavBar = ({ navigationItems, agent }: LayoutNavigationProps)
<Navbar.Section mx="md" className={classes.layoutAvatar}>
<Flex gap="xs" justify="space-between">
<LayoutAvatar agent={agent} />
<Menu shadow="md" width={200} position="bottom-end">
<Menu shadow="md" position="bottom-end" withArrow>
<Menu.Target>
<ActionIcon mt="xs">
<IconChevronDown />
<IconDotsVertical size={20} />
</ActionIcon>
</Menu.Target>

Expand Down
46 changes: 32 additions & 14 deletions packages/siera-ui/src/modals/ConfirmActionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,62 @@ import type { ContextModalProps } from '@mantine/modals'

import { Divider, Group, Stack, Text } from '@mantine/core'
import { openContextModal } from '@mantine/modals'
import React from 'react'
import React, { useState } from 'react'

import { PrimaryButton, SecondaryButton } from '../components/generic'

type InnerProps = {
message: string
onConfirm: () => void
description: string
onConfirm: () => Promise<void> | void
confirmLabel?: string
}

export const ConfirmActionModal = ({
id,
context,
innerProps: { message, onConfirm },
innerProps: { description, onConfirm, confirmLabel },
}: ContextModalProps<InnerProps>) => {
const [isSubmitting, setIsSubmitting] = useState(false)
const onConfirmClick = async () => {
setIsSubmitting(true)
try {
await onConfirm()
setIsSubmitting(false)
context.closeModal(id)
} catch (error) {
setIsSubmitting(false)
throw error
}
}

return (
<Stack mt="md">
<Text px="xl">{message}</Text>
<Text px="xl">{description}</Text>
<Divider mt="md" mb="xs" />
<Group position="right" px="xl">
<SecondaryButton onClick={() => context.closeModal(id)}>Cancel</SecondaryButton>
<PrimaryButton
onClick={() => {
onConfirm()
context.closeModal(id)
}}
>
Confirm
<PrimaryButton onClick={onConfirmClick} loading={isSubmitting}>
{confirmLabel}
</PrimaryButton>
</Group>
</Stack>
)
}

export const openConfirmActionModal = (title: string, message: string, onConfirm: () => void) => {
interface OpenConfirmActionModalProps extends InnerProps {
title: string
}

export const openConfirmActionModal = ({
title,
description,
onConfirm,
confirmLabel = 'Confirm',
}: OpenConfirmActionModalProps) => {
openContextModal({
modal: ConfirmActionModal.name,
title,
innerProps: { message, onConfirm },
innerProps: { description: description, onConfirm, confirmLabel },
centered: true,
withCloseButton: false,
})
Expand Down
3 changes: 0 additions & 3 deletions packages/siera-ui/src/modals/index.ts

This file was deleted.

39 changes: 34 additions & 5 deletions packages/siera-ui/src/pages/AgentSelectionScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { Box, Card, Container, createStyles, Flex, Text, Title, UnstyledButton } from '@mantine/core'
import type { AgentConfigRecord } from '@animo/siera-core'

import { ActionIcon, Box, Card, Container, createStyles, Flex, Menu, Text, Title, UnstyledButton } from '@mantine/core'
import { IconDotsVertical } from '@tabler/icons'
import React from 'react'

import { Loading } from '../components/Loading'
import { SmartAvatar } from '../components/SmartAvatar'
import { PrimaryButton } from '../components/generic'
import { useAgentManager } from '../contexts/AgentManagerContext'
import { useNavigation } from '../hooks/useNavigation'
import { openCreateAgentModal } from '../modals'
import { openConfirmActionModal } from '../modals/ConfirmActionModal'
import { openCreateAgentModal } from '../modals/CreateAgentModal'

import { WelcomeScreen } from './agent/WelcomeScreen'

const useStyles = createStyles((theme) => ({
card: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.backgroundOne[7] : '#ffffff',
border: `2px solid ${theme.colors.backgroundOne[6]}`,
overflow: 'unset',

'&:hover': {
border: `2px solid ${theme.colors.backgroundOne[5]}`,
Expand All @@ -33,13 +38,24 @@ const useStyles = createStyles((theme) => ({
export const AgentSelectionScreen = () => {
const { classes } = useStyles()
const navigation = useNavigation()
const { agents, setCurrentAgentId, loading } = useAgentManager()
const { agents, setCurrentAgentId, removeAgent, loading } = useAgentManager()

const switchToAgent = (agentId: string) => {
setCurrentAgentId(agentId)
navigation.navigate('/agent/connections')
}

const stopPropagation = (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => event.stopPropagation()

const onDeleteAgent = (agentConfigRecord: AgentConfigRecord) => {
openConfirmActionModal({
title: 'Delete agent',
description: `Are you sure you want to delete '${agentConfigRecord.name}' ?`,
confirmLabel: 'Delete',
onConfirm: async () => await removeAgent(agentConfigRecord.id),
})
}

if (loading) {
return <Loading description="Loading configuration" />
}
Expand All @@ -64,8 +80,8 @@ export const AgentSelectionScreen = () => {
<Flex gap="md" wrap="wrap" mt="xl">
{agents.map((agent) => (
<UnstyledButton key={agent.id} onClick={() => switchToAgent(agent.id)}>
<Card h={80} w={220} className={classes.card} radius="md">
<Flex gap="sm" maw="100%" wrap="nowrap">
<Card h={80} w={220} className={classes.card} radius="md" p={0}>
<Flex gap="sm" maw="100%" align="center" wrap="nowrap" pl="sm" mih="100%">
<SmartAvatar src={agent.agentConfig.connectionImageUrl} size={38} radius="xl">
{agent.agentConfig.label}
</SmartAvatar>
Expand All @@ -77,6 +93,19 @@ export const AgentSelectionScreen = () => {
Native (AFJ)
</Text>
</Box>
<Flex style={{ flex: 'auto', alignSelf: 'start' }} justify="end">
<Menu shadow="md" position="bottom-end" withArrow>
<Menu.Target>
<ActionIcon mt={2} onClick={stopPropagation} variant="transparent">
<IconDotsVertical size={16} />
</ActionIcon>
</Menu.Target>

<Menu.Dropdown onClick={stopPropagation}>
<Menu.Item onClick={() => onDeleteAgent(agent)}>Delete</Menu.Item>
</Menu.Dropdown>
</Menu>
</Flex>
</Flex>
</Card>
</UnstyledButton>
Expand Down
Loading