Skip to content

Commit f24020f

Browse files
author
colinmcneil
committed
Implement prompt configs
1 parent 6ba69df commit f24020f

File tree

9 files changed

+334
-48
lines changed

9 files changed

+334
-48
lines changed

src/extension/ui/package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"@mui/icons-material": "6.4.5",
1212
"@mui/material": "6.4.5",
1313
"ansi-to-html": "^0.7.2",
14+
"json-edit-react": "^1.23.1",
1415
"react": "^18.2.0",
1516
"react-dom": "^18.2.0",
1617
"yaml": "^2.3.1"

src/extension/ui/src/App.tsx

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { POLL_INTERVAL } from './Constants';
1010
import MCPCatalogLogo from './MCP Catalog.svg'
1111
import Settings from './components/Settings';
1212
import { getMCPClientStates, MCPClientState } from './MCPClients';
13+
import PromptConfig from './components/PromptConfig';
1314

1415
export const client = createDockerDesktopClient();
1516

@@ -20,10 +21,12 @@ const DEFAULT_SETTINGS = {
2021

2122
export function App() {
2223
const [canRegister, setCanRegister] = useState(false);
23-
const [registryItems, setRegistryItems] = useState<{ [key: string]: { ref: string } }>({});
24+
const [registryItems, setRegistryItems] = useState<{ [key: string]: { ref: string; config: any } }>({});
2425
const [imagesLoadingResults, setImagesLoadingResults] = useState<ExecResult | null>(null);
2526
const [settings, setSettings] = useState<{ showModal: boolean, pollIntervalSeconds: number }>(localStorage.getItem('settings') ? JSON.parse(localStorage.getItem('settings') || '{}') : DEFAULT_SETTINGS);
2627
const [mcpClientStates, setMcpClientStates] = useState<{ [name: string]: MCPClientState }>({});
28+
const [configuringItem, setConfiguringItem] = useState<CatalogItemWithName | null>(null);
29+
2730
const loadRegistry = async () => {
2831
setCanRegister(false);
2932
try {
@@ -70,15 +73,21 @@ export function App() {
7073
}
7174

7275
useEffect(() => {
73-
startImagesLoading();
74-
loadRegistry();
75-
updateMCPClientStates();
76-
const interval = setInterval(() => {
76+
let interval: NodeJS.Timeout | null = null;
77+
78+
startImagesLoading().then(() => {
7779
loadRegistry();
7880
updateMCPClientStates();
79-
}, POLL_INTERVAL);
81+
interval = setInterval(() => {
82+
loadRegistry();
83+
updateMCPClientStates();
84+
}, POLL_INTERVAL);
85+
})
86+
8087
return () => {
81-
clearInterval(interval)
88+
if (interval) {
89+
clearInterval(interval)
90+
}
8291
}
8392
}, []);
8493

@@ -103,20 +112,37 @@ export function App() {
103112
<Settings onUpdate={updateMCPClientStates} mcpClientStates={mcpClientStates} settings={settings} setSettings={setSettings} />
104113
</DialogContent>
105114
</Dialog>
115+
{configuringItem && <Dialog open={configuringItem !== null} onClose={() => setConfiguringItem(null)}>
116+
<DialogTitle>
117+
<Typography variant="h6">
118+
Config
119+
</Typography>
120+
</DialogTitle>
121+
<DialogContent>
122+
<PromptConfig client={client} catalogItem={configuringItem!} registryItem={registryItems[configuringItem!.name]} onRegistryChange={loadRegistry} />
123+
</DialogContent>
124+
</Dialog>}
106125
<Stack direction="column" spacing={1} justifyContent='center' alignItems='center'>
107126
<img src={MCPCatalogLogo} alt="MCP Catalog" height={100} />
108127
{hasMCPConfigured ? <></> : <Alert action={<Button variant='outlined' color='secondary' onClick={() => setSettings({ ...settings, showModal: true })}>Configure</Button>} severity="error" sx={{ fontWeight: 'bold' }}>MCP Clients are not configured. Please configure MCP Clients to use the MCP Catalog.</Alert>}
109-
<CatalogGrid settingsBadgeProps={hasMCPConfigured ? {} : {
110-
color: hasMCPConfigured ? 'default' : 'error',
111-
badgeContent: '0 MCP Clients',
112-
sx: {
113-
width: 80,
114-
height: '100%',
115-
display: 'flex',
116-
justifyContent: 'center',
117-
alignItems: 'center',
118-
},
119-
}} showSettings={() => setSettings({ ...settings, showModal: true })} registryItems={registryItems} canRegister={canRegister} client={client} onRegistryChange={loadRegistry} />
128+
<CatalogGrid
129+
settingsBadgeProps={hasMCPConfigured ? {} : {
130+
color: hasMCPConfigured ? 'default' : 'error',
131+
badgeContent: '0 MCP Clients',
132+
sx: {
133+
width: 80,
134+
height: '100%',
135+
display: 'flex',
136+
justifyContent: 'center',
137+
alignItems: 'center',
138+
},
139+
}}
140+
setConfiguringItem={setConfiguringItem}
141+
showSettings={() => setSettings({ ...settings, showModal: true })}
142+
registryItems={registryItems}
143+
canRegister={canRegister}
144+
client={client}
145+
onRegistryChange={loadRegistry} />
120146
</Stack>
121147
</>
122148
)

src/extension/ui/src/MergeDeep.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Simple object check.
3+
* @param item
4+
* @returns {boolean}
5+
*/
6+
export function isObject(item: any) {
7+
return (item && typeof item === 'object' && !Array.isArray(item));
8+
}
9+
10+
/**
11+
* Deep merge two objects.
12+
* @param target
13+
* @param ...sources
14+
*/
15+
export function mergeDeep(target: any, ...sources: any[]) {
16+
if (!sources.length) return target;
17+
const source = sources.shift();
18+
19+
if (isObject(target) && isObject(source)) {
20+
for (const key in source) {
21+
if (isObject(source[key])) {
22+
if (!target[key]) Object.assign(target, { [key]: {} });
23+
mergeDeep(target[key], source[key]);
24+
} else {
25+
Object.assign(target, { [key]: source[key] });
26+
}
27+
}
28+
}
29+
30+
return mergeDeep(target, ...sources);
31+
}

src/extension/ui/src/Registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const getRegistry = async (client: v1.DockerDesktopClient) => {
66
const parseRegistry = async () => {
77
const registry = await readFileInPromptsVolume(client, 'registry.yaml')
88
if (registry) {
9-
return parse(registry)['registry'] as Promise<{ [key: string]: { ref: string } }>;
9+
return parse(registry)['registry'] as Promise<{ [key: string]: { ref: string; config: any } }>;
1010
}
1111
return {};
1212
}

src/extension/ui/src/components/CatalogGrid.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface CatalogGridProps {
1919
onRegistryChange: () => void;
2020
showSettings: () => void;
2121
settingsBadgeProps: BadgeProps;
22+
setConfiguringItem: (item: CatalogItemWithName) => void;
2223
}
2324

2425
const filterCatalog = (catalogItems: CatalogItemWithName[], registryItems: { [key: string]: { ref: string } }, search: string) =>
@@ -40,7 +41,8 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
4041
client,
4142
onRegistryChange,
4243
showSettings,
43-
settingsBadgeProps
44+
settingsBadgeProps,
45+
setConfiguringItem
4446
}) => {
4547
const [catalogItems, setCatalogItems] = useState<CatalogItemWithName[]>([]);
4648
const [showReloadModal, setShowReloadModal] = useState<boolean>(false);
@@ -207,6 +209,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
207209
{filteredCatalogItems.map((item) => (
208210
<Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={item.name}>
209211
<CatalogItemCard
212+
setConfiguringItem={setConfiguringItem}
210213
openUrl={() => {
211214
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
212215
}}
@@ -244,7 +247,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
244247
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} onSecretChange={async (secret) => {
245248
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
246249
loadSecrets();
247-
}} secrets={secrets} />
250+
}} secrets={secrets} setConfiguringItem={setConfiguringItem} />
248251
</Grid2>
249252
))}
250253
</Grid2>}

src/extension/ui/src/components/PromptCard.tsx

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { Badge, CircularProgress, Dialog, DialogContent, DialogTitle, Divider, IconButton, List, ListItem, ListItemIcon, ListItemText, Stack, Switch, TextField, Tooltip } from "@mui/material";
1+
import { Badge, CircularProgress, Dialog, DialogContent, DialogTitle, Divider, IconButton, List, ListItem, ListItemIcon, ListItemText, Stack, Switch, TextField, Tooltip, useTheme } from "@mui/material";
22
import Button from '@mui/material/Button';
33
import { Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material";
44
import { Ref } from "../Refs";
55
import { useEffect, useState } from "react";
66
import { trackEvent } from "../Usage";
7-
import { Article, AttachFile, Build, CheckBox, Delete, LockOpenRounded, LockReset, LockRounded, NoEncryptionGmailerrorred, Save } from "@mui/icons-material";
7+
import { Article, AttachFile, Build, CheckBox, Delete, LockOpenRounded, LockReset, LockRounded, NoEncryptionGmailerrorred, Save, Settings } from "@mui/icons-material";
88
import Secrets from "../Secrets";
99
import { DD_BUILD_WITH_SECRET_SUPPORT, getUnsupportedSecretMessage } from "../Constants";
10-
10+
import { DataType, githubDarkTheme, githubLightTheme, JsonEditor, NodeData } from "json-edit-react";
11+
import PromptConfig, { Config } from "./PromptConfig";
1112
const iconSize = 16
1213

14+
15+
1316
export interface CatalogItem {
1417
description?: string;
1518
icon?: string;
@@ -18,13 +21,15 @@ export interface CatalogItem {
1821
prompts: number;
1922
resources: object[];
2023
tools: object[];
24+
config?: Config;
2125
}
2226

2327
export interface CatalogItemWithName extends CatalogItem {
2428
name: string;
2529
}
2630

27-
export function CatalogItemCard({ openUrl, item, canRegister, registered, register, unregister, onSecretChange, secrets, ddVersion }: { openUrl: () => void, item: CatalogItemWithName, canRegister: boolean, registered: boolean, register: (item: CatalogItemWithName) => Promise<void>, unregister: (item: CatalogItemWithName, showNotification?: boolean) => Promise<void>, onSecretChange: (secret: { name: string, value: string }) => Promise<void>, secrets: Secrets.Secret[], ddVersion: { version: string, build: number } }) {
31+
32+
export function CatalogItemCard({ setConfiguringItem, openUrl, item, canRegister, registered, register, unregister, onSecretChange, secrets, ddVersion }: { setConfiguringItem: (item: CatalogItemWithName) => void, openUrl: () => void, item: CatalogItemWithName, canRegister: boolean, registered: boolean, register: (item: CatalogItemWithName) => Promise<void>, unregister: (item: CatalogItemWithName, showNotification?: boolean) => Promise<void>, onSecretChange: (secret: { name: string, value: string }) => Promise<void>, secrets: Secrets.Secret[], ddVersion: { version: string, build: number } }) {
2833
const loadAssignedSecrets = () => {
2934
const assignedSecrets = Secrets.getAssignedSecrets(item, secrets);
3035
setAssignedSecrets(assignedSecrets)
@@ -152,29 +157,39 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
152157
</Tooltip>
153158
))}
154159
</Stack>
155-
<Tooltip open={tooltipOpen} onClose={() => setTooltipOpen(false)} onOpen={() => setTooltipOpen(true)} title={hasAllSecrets ? registered ? "Blocking this tile will remove its tools, resources and prompts from being used in any MCP clients you have connected." : "Allowing this tile will expose its tools, resources and prompts to any MCP clients you have connected." : "You need to set all expected secrets to allow this tile."}>
156-
{!hasAllSecrets ? <LockRounded /> : isRegistering ? <CircularProgress size={20} /> : <Switch
157-
size="small"
158-
color={registered ? 'success' : 'primary'}
159-
checked={registered && hasAllSecrets}
160-
onChange={() => {
161-
setTooltipOpen(false)
162-
trackEvent('registry-changed', { name: item.name, ref: item.ref, action: registered ? 'remove' : 'add' });
163-
setIsRegistering(true)
164-
if (registered) {
165-
unregister(item).then(() => {
166-
setIsRegistering(false)
167-
})
168-
} else {
169-
register(item).then(() => {
170-
setIsRegistering(false)
171-
})
172-
}
160+
<Stack direction="row" spacing={1} alignItems="center" justifyContent="flex-end">
161+
{/* WIP */}
162+
{item.config && registered && (
163+
<Tooltip title="Configure this item">
164+
<IconButton onClick={() => setConfiguringItem(item)}>
165+
<Settings />
166+
</IconButton>
167+
</Tooltip>
168+
)}
169+
<Tooltip open={tooltipOpen} onClose={() => setTooltipOpen(false)} onOpen={() => setTooltipOpen(true)} title={hasAllSecrets ? registered ? "Blocking this tile will remove its tools, resources and prompts from being used in any MCP clients you have connected." : "Allowing this tile will expose its tools, resources and prompts to any MCP clients you have connected." : "You need to set all expected secrets to allow this tile."}>
170+
{!hasAllSecrets ? <LockRounded /> : isRegistering ? <CircularProgress size={20} /> : <Switch
171+
size="small"
172+
color={registered ? 'success' : 'primary'}
173+
checked={registered && hasAllSecrets}
174+
onChange={() => {
175+
setTooltipOpen(false)
176+
trackEvent('registry-changed', { name: item.name, ref: item.ref, action: registered ? 'remove' : 'add' });
177+
setIsRegistering(true)
178+
if (registered) {
179+
unregister(item).then(() => {
180+
setIsRegistering(false)
181+
})
182+
} else {
183+
register(item).then(() => {
184+
setIsRegistering(false)
185+
})
186+
}
187+
}}
188+
disabled={!canRegister || isRegistering}
189+
/>}
190+
</Tooltip>
173191

174-
}}
175-
disabled={!canRegister || isRegistering}
176-
/>}
177-
</Tooltip>
192+
</Stack>
178193
</Stack>
179194

180195
</CardActions>

0 commit comments

Comments
 (0)