Skip to content

Commit

Permalink
feat: sortable plugins (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
simonas-notcat authored Aug 30, 2023
1 parent dd4382b commit c819b9d
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 65 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
]
},
"dependencies": {
"@dnd-kit/utilities": "^3.2.1",
"commander": "^10.0.1",
"express": "^4.18.2",
"express-favicon": "^2.0.4"
Expand Down Expand Up @@ -124,6 +125,8 @@
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"@craco/craco": "^7.1.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@ethersproject/providers": "^5.7.2",
"@semantic-release/commit-analyzer": "^10.0.1",
"@semantic-release/git": "^10.0.1",
Expand Down
14 changes: 9 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/context/PluginProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const corePlugins = getcorePlugins()
type PluginContextType = {
plugins: AgentPlugin[]
pluginConfigs: PluginConfig[]
updatePluginConfigs: (configs: PluginConfig[]) => void
addPluginConfig: (config: PluginConfig) => void
removePluginConfig: (url: string) => void
switchPlugin: (url: string, enabled: boolean) => void
Expand All @@ -15,6 +16,7 @@ type PluginContextType = {
const PluginContext = createContext<PluginContextType>({
plugins: [],
pluginConfigs: [],
updatePluginConfigs: () => null,
addPluginConfig: () => null,
removePluginConfig: () => null,
switchPlugin: () => null,
Expand Down Expand Up @@ -103,11 +105,17 @@ const PluginProvider = (props: any) => {
setPluginConfigs(configs)
}

const updatePluginConfigs = (configs: PluginConfig[]) => {
storePluginConfigs(configs)
setPluginConfigs(configs)
}

return (
<PluginContext.Provider
value={{
plugins,
pluginConfigs,
updatePluginConfigs,
addPluginConfig,
removePluginConfig,
switchPlugin,
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as reactrouterdom from 'react-router-dom'
import * as uuid from 'uuid'
import * as datefns from 'date-fns'
import * as reactjsxruntime from 'react/jsx-runtime'

import './style.css'
// Global variables for plugins
declare global {
interface Window {
Expand Down
182 changes: 123 additions & 59 deletions src/pages/settings/Plugins.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,139 @@
import React, { useState } from 'react'
import { Button, Input, List, Space, Switch, App, Drawer } from 'antd'
import { DeleteOutlined, PlusOutlined} from '@ant-design/icons'
import { DeleteOutlined, MenuOutlined, PlusOutlined} from '@ant-design/icons'
import { usePlugins } from '../../context/PluginProvider'
import { PageContainer } from '@ant-design/pro-components'
import { DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay
} from '@dnd-kit/core';
import { SortableContext, useSortable, verticalListSortingStrategy, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import {CSS} from '@dnd-kit/utilities';
import { AgentPlugin } from '../../types'

export const Plugins = () => {
const SortableItem = ({ item }: { item: AgentPlugin}) => {
const { notification } = App.useApp()
const { removePluginConfig, switchPlugin } = usePlugins()
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: item.config.url });
const actions: React.ReactNode[] = []
if (!item.config.url.startsWith('core://')) {
actions.push(<Button
icon={<DeleteOutlined />}
danger
type="text"
onClick={() => {
if (window.confirm(`Delete ${item.name}`)) {
removePluginConfig(item.config.url)
notification.success({
message: 'Plugin removed',
})
}
}}
/>)
}

actions.push(<Switch checked={item.config.enabled} onChange={(checked) => switchPlugin(item.config.url, checked)} />)
actions.push(<MenuOutlined ref={setNodeRef} {...attributes} {...listeners} className="draggable-item"/>)
return (
<List.Item
ref={setNodeRef}
style={{ transform: CSS.Transform.toString(transform), transition }}
actions={actions}
><List.Item.Meta
title={item.name}
description={item.description}
/>
</List.Item>
);
};

export const Plugins = () => {
const [isDrawerOpen, setDrawerOpen] = useState(false);
const { addPluginConfig, plugins, removePluginConfig, switchPlugin } = usePlugins()
const { addPluginConfig, plugins, updatePluginConfigs } = usePlugins()
const [url, setUrl] = React.useState('')

const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);

const handleDragEnd = (event: any) => {
const { active, over } = event;

if (active.id !== over.id) {
const activeIndex = plugins.findIndex((plugin) => plugin.config.url === active.id);
const overIndex = plugins.findIndex((plugin) => plugin.config.url === over.id);

if (activeIndex !== -1 && overIndex !== -1) {
const reorderedPlugins = [...plugins];
[reorderedPlugins[activeIndex], reorderedPlugins[overIndex]] = [reorderedPlugins[overIndex], reorderedPlugins[activeIndex]];

updatePluginConfigs(reorderedPlugins.map((plugin) => plugin.config));
}
}
};



return (
<PageContainer
extra={[
<Button
key={'add'}
icon={<PlusOutlined />}
type="primary"
title="Add new external plugin"
onClick={() => setDrawerOpen(true)}
/>,
]}
<DndContext
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={closestCenter}
>
<List
dataSource={plugins}
renderItem={(item) => <List.Item
actions={[
<Switch checked={item.config.enabled} onChange={(checked) => switchPlugin(item.config.url, checked)} />,
!item.config.url.startsWith('core://') && <Button
icon={<DeleteOutlined />}
danger
type="text"
<PageContainer
extra={[
<Button
key={'add'}
icon={<PlusOutlined />}
type="primary"
title="Add new external plugin"
onClick={() => setDrawerOpen(true)}
/>,
]}
>

<SortableContext
items={plugins.map((plugin) => plugin.config.url)}
strategy={verticalListSortingStrategy}
>
<List
dataSource={plugins}
renderItem={(item) => <SortableItem item={item} key={item.config.url}/>}
/>
</SortableContext>
<DragOverlay />

<Drawer
title="Add external plugin"
placement={'right'}
width={500}
onClose={() => setDrawerOpen(false)}
open={isDrawerOpen}
>
<Space.Compact style={{ width: '100%' }}>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<Button
type="primary"
onClick={() => {
if (window.confirm(`Delete ${item.name}`)) {
removePluginConfig(item.config.url)
notification.success({
message: 'Plugin removed',
})
}
setDrawerOpen(false)
addPluginConfig({url, enabled: true})
setUrl('')
}}
/>
]}
><List.Item.Meta
title={item.name}
description={item.description}
/></List.Item>}
/>

<Drawer
title="Add external plugin"
placement={'right'}
width={500}
onClose={() => setDrawerOpen(false)}
open={isDrawerOpen}
>
<Space.Compact style={{ width: '100%' }}>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<Button
type="primary"
onClick={() => {
setDrawerOpen(false)
addPluginConfig({url, enabled: true})
setUrl('')
}}
>Add</Button>
</Space.Compact>
</Drawer>
</PageContainer>
>Add</Button>
</Space.Compact>
</Drawer>
</PageContainer>
</DndContext>
)
}

6 changes: 6 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.draggable-item {
cursor: grab;
}
.draggable-item:active {
cursor: grabbing;
}

0 comments on commit c819b9d

Please sign in to comment.