Skip to content

Commit e2d5d39

Browse files
committed
Finalize UI for config changing with config.yaml
1 parent f24020f commit e2d5d39

File tree

6 files changed

+229
-123
lines changed

6 files changed

+229
-123
lines changed

src/extension/ui/src/App.tsx

+24-16
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import React, { useEffect, useState } from 'react';
22
import { createDockerDesktopClient } from '@docker/extension-api-client';
33
import { Stack, Typography, Button, ButtonGroup, Grid, debounce, Card, CardContent, IconButton, Alert, DialogTitle, Dialog, DialogContent, FormControlLabel, Checkbox, CircularProgress, Paper, DialogActions, Box } from '@mui/material';
44
import { CatalogItemWithName } from './components/PromptCard';
5-
import { getRegistry } from './Registry';
5+
import { getRegistry, getStoredConfig, syncConfigWithRegistry, syncRegistryWithConfig } from './Registry';
66
import { Close, FolderOpenRounded, } from '@mui/icons-material';
77
import { ExecResult } from '@docker/extension-api-client-types/dist/v0';
88
import { CatalogGrid } from './components/CatalogGrid';
99
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';
13+
import PromptConfig, { ParsedParameters } from './components/PromptConfig';
1414

1515
export const client = createDockerDesktopClient();
1616

@@ -26,6 +26,7 @@ export function App() {
2626
const [settings, setSettings] = useState<{ showModal: boolean, pollIntervalSeconds: number }>(localStorage.getItem('settings') ? JSON.parse(localStorage.getItem('settings') || '{}') : DEFAULT_SETTINGS);
2727
const [mcpClientStates, setMcpClientStates] = useState<{ [name: string]: MCPClientState }>({});
2828
const [configuringItem, setConfiguringItem] = useState<CatalogItemWithName | null>(null);
29+
const [config, setConfig] = useState<{ [key: string]: { [key: string]: ParsedParameters } }>({});
2930

3031
const loadRegistry = async () => {
3132
setCanRegister(false);
@@ -72,23 +73,28 @@ export function App() {
7273
}
7374
}
7475

75-
useEffect(() => {
76-
let interval: NodeJS.Timeout | null = null;
76+
const loadConfig = async () => {
77+
const config = await getStoredConfig(client);
78+
setConfig(config);
79+
}
80+
81+
const startSyncing = async () => {
82+
await updateMCPClientStates();
83+
await syncConfigWithRegistry(client);
84+
await syncRegistryWithConfig(client);
85+
await loadRegistry();
86+
await loadConfig();
87+
}
7788

89+
useEffect(() => {
90+
let interval: NodeJS.Timeout;
7891
startImagesLoading().then(() => {
79-
loadRegistry();
80-
updateMCPClientStates();
92+
startSyncing();
8193
interval = setInterval(() => {
82-
loadRegistry();
83-
updateMCPClientStates();
94+
startSyncing();
8495
}, POLL_INTERVAL);
8596
})
86-
87-
return () => {
88-
if (interval) {
89-
clearInterval(interval)
90-
}
91-
}
97+
return () => clearInterval(interval);
9298
}, []);
9399

94100
if (!imagesLoadingResults || imagesLoadingResults.stderr) {
@@ -115,7 +121,7 @@ export function App() {
115121
{configuringItem && <Dialog open={configuringItem !== null} onClose={() => setConfiguringItem(null)}>
116122
<DialogTitle>
117123
<Typography variant="h6">
118-
Config
124+
Config for {configuringItem.name}
119125
</Typography>
120126
</DialogTitle>
121127
<DialogContent>
@@ -142,7 +148,9 @@ export function App() {
142148
registryItems={registryItems}
143149
canRegister={canRegister}
144150
client={client}
145-
onRegistryChange={loadRegistry} />
151+
onRegistryChange={loadRegistry}
152+
config={config}
153+
/>
146154
</Stack>
147155
</>
148156
)

src/extension/ui/src/FileWatcher.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import { ExecResult } from "@docker/extension-api-client-types/dist/v0"
77

88
const allWatches: { [key: string]: any } = {}
99

10-
export const tryRunImageSync = async (client: v1.DockerDesktopClient, args: string[]) => {
11-
const showError = client.desktopUI.toast.error
10+
export const tryRunImageSync = async (client: v1.DockerDesktopClient, args: string[], ignoreError = false) => {
11+
const showError = ignoreError ? () => { } : client.desktopUI.toast.error
1212
try {
1313
const result = await client.docker.cli.exec('run', args)
1414
if (result.stderr) {
1515
console.error(result.stderr)
1616
showError(result.stderr)
1717
}
18-
return result.stdout
18+
return result.stdout || ''
1919
}
2020
catch (e) {
2121
if (e instanceof Error) {
@@ -34,7 +34,7 @@ export const getUser = async (client: v1.DockerDesktopClient) => {
3434
}
3535

3636
export const readFileInPromptsVolume = async (client: v1.DockerDesktopClient, path: string) => {
37-
return tryRunImageSync(client, ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'alpine:latest', 'sh', '-c', `"cat ${path}"`])
37+
return tryRunImageSync(client, ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'alpine:latest', 'sh', '-c', `"cat ${path}"`], true)
3838
}
3939

4040
export const writeFileToPromptsVolume = async (client: v1.DockerDesktopClient, content: string) => {

src/extension/ui/src/Registry.ts

+69-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import { v1 } from "@docker/extension-api-client-types";
2-
import { parse } from "yaml";
2+
import { parse, stringify } from "yaml";
33
import { readFileInPromptsVolume, writeFileToPromptsVolume } from "./FileWatcher";
4+
import { ParsedParameters } from "./components/PromptConfig";
5+
import { mergeDeep } from "./MergeDeep";
46

57
export const getRegistry = async (client: v1.DockerDesktopClient) => {
68
const parseRegistry = async () => {
79
const registry = await readFileInPromptsVolume(client, 'registry.yaml')
810
if (registry) {
9-
return parse(registry)['registry'] as Promise<{ [key: string]: { ref: string; config: any } }>;
11+
const value = parse(registry)['registry'] as { [key: string]: { ref: string, config: any } }
12+
if (!value) {
13+
client.desktopUI.toast.error('Failed to get parse registry.yaml.registry: ' + registry)
14+
}
15+
return value;
1016
}
1117
return {};
1218
}
1319
const writeRegistryIfNotExists = async () => {
1420
const registry = await readFileInPromptsVolume(client, 'registry.yaml')
15-
1621
if (!registry) {
1722
console.log('writeRegistryIfNotExists: no registry')
1823
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'registry.yaml', content: 'registry: {}' }] }))
@@ -26,4 +31,64 @@ export const getRegistry = async (client: v1.DockerDesktopClient) => {
2631
client.desktopUI.toast.error('Failed to get prompt registry: ' + error)
2732
return {};
2833
}
29-
}
34+
}
35+
36+
export const getStoredConfig = async (client: v1.DockerDesktopClient) => {
37+
const parseConfig = async () => {
38+
const config = await readFileInPromptsVolume(client, 'config.yaml')
39+
if (config) {
40+
return parse(config) as Promise<{ [key: string]: { [key: string]: ParsedParameters } }>;
41+
}
42+
return {};
43+
}
44+
const writeConfigIfNotExists = async () => {
45+
const config = await readFileInPromptsVolume(client, 'config.yaml')
46+
if (!config) {
47+
console.log('writeConfigIfNotExists: no config')
48+
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'config.yaml', content: '{}' }] }))
49+
}
50+
}
51+
try {
52+
await writeConfigIfNotExists()
53+
return await parseConfig()
54+
}
55+
catch (error) {
56+
client.desktopUI.toast.error('Failed to get stored configs: ' + error)
57+
return {};
58+
}
59+
}
60+
61+
// if registry.yaml has a config, it must be the same as what you have stored
62+
// if that’s not true and the registry.yaml value is valid then you should sync with it
63+
// if it’s not true and the registry.yaml is invalid then the catalog item needs user assistance because the catalog has probably been updated with a breaking change
64+
65+
// Replace conflicting config values with registry values
66+
export const syncConfigWithRegistry = async (client: v1.DockerDesktopClient) => {
67+
const registry = await getRegistry(client) || {}
68+
const storedConfig = await getStoredConfig(client) || {}
69+
70+
for (const [key, item] of Object.entries(registry)) {
71+
const itemConfig = item.config || {}
72+
const storedConfigItem = storedConfig[key]
73+
if (storedConfigItem) {
74+
const mergedConfig = mergeDeep(storedConfigItem, itemConfig)
75+
storedConfig[key] = mergedConfig
76+
}
77+
}
78+
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'config.yaml', content: stringify(storedConfig) }] }))
79+
}
80+
81+
// Replace conflicting registry values with config values
82+
export const syncRegistryWithConfig = async (client: v1.DockerDesktopClient) => {
83+
const storedRegistry = await getRegistry(client) || {}
84+
const storedConfig = await getStoredConfig(client) || {}
85+
for (const [key, item] of Object.entries(storedConfig)) {
86+
const itemConfig = item || {}
87+
const registryItem = storedRegistry[key]
88+
if (registryItem) {
89+
const mergedConfig = mergeDeep(registryItem.config, itemConfig)
90+
storedRegistry[key].config = mergedConfig
91+
}
92+
}
93+
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'registry.yaml', content: stringify({ registry: storedRegistry }) }] }))
94+
}

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

+49-34
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@ import AddIcon from '@mui/icons-material/Add';
55
import { Ref } from '../Refs';
66
import { v1 } from "@docker/extension-api-client-types";
77
import { parse, stringify } from 'yaml';
8-
import { getRegistry } from '../Registry';
8+
import { getRegistry, syncConfigWithRegistry, syncRegistryWithConfig } from '../Registry';
99
import { FolderOpenRounded, Search, Settings } from '@mui/icons-material';
1010
import { tryRunImageSync } from '../FileWatcher';
1111
import { CATALOG_URL, DD_BUILD_WITH_SECRET_SUPPORT, MCP_POLICY_NAME, POLL_INTERVAL } from '../Constants';
1212
import { SecretList } from './SecretList';
1313
import Secrets from '../Secrets';
14+
import { ParsedParameters } from './PromptConfig';
1415

1516
interface CatalogGridProps {
16-
registryItems: { [key: string]: { ref: string } };
17+
registryItems: { [key: string]: { ref: string, config: any } };
1718
canRegister: boolean;
1819
client: v1.DockerDesktopClient;
1920
onRegistryChange: () => void;
2021
showSettings: () => void;
2122
settingsBadgeProps: BadgeProps;
2223
setConfiguringItem: (item: CatalogItemWithName) => void;
24+
config: { [key: string]: { [key: string]: ParsedParameters } };
2325
}
2426

2527
const filterCatalog = (catalogItems: CatalogItemWithName[], registryItems: { [key: string]: { ref: string } }, search: string) =>
@@ -42,7 +44,8 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
4244
onRegistryChange,
4345
showSettings,
4446
settingsBadgeProps,
45-
setConfiguringItem
47+
setConfiguringItem,
48+
config
4649
}) => {
4750
const [catalogItems, setCatalogItems] = useState<CatalogItemWithName[]>([]);
4851
const [showReloadModal, setShowReloadModal] = useState<boolean>(false);
@@ -105,6 +108,8 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
105108
if (showNotification) {
106109
setShowReloadModal(!localStorage.getItem(NEVER_SHOW_AGAIN_KEY));
107110
}
111+
await syncConfigWithRegistry(client);
112+
await syncRegistryWithConfig(client);
108113
}
109114
catch (error) {
110115
if (showNotification) {
@@ -127,6 +132,8 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
127132
client.desktopUI.toast.success('Prompt unregistered successfully. Restart Claude Desktop to apply.');
128133
onRegistryChange();
129134
setShowReloadModal(!localStorage.getItem(NEVER_SHOW_AGAIN_KEY));
135+
await syncConfigWithRegistry(client);
136+
await syncRegistryWithConfig(client);
130137
}
131138
catch (error) {
132139
client.desktopUI.toast.error('Failed to unregister prompt: ' + error)
@@ -206,27 +213,32 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
206213
</FormGroup >
207214

208215
{tab === 0 && <Grid2 container spacing={1} width='90vw' maxWidth={1000}>
209-
{filteredCatalogItems.map((item) => (
210-
<Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={item.name}>
211-
<CatalogItemCard
212-
setConfiguringItem={setConfiguringItem}
213-
openUrl={() => {
214-
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
215-
}}
216-
item={item}
217-
ddVersion={ddVersion}
218-
canRegister={canRegister}
219-
registered={Object.keys(registryItems).some((i) => i === item.name)}
220-
register={registerCatalogItem}
221-
unregister={unregisterCatalogItem}
222-
onSecretChange={async (secret) => {
223-
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
224-
loadSecrets();
225-
}}
226-
secrets={secrets}
227-
/>
228-
</Grid2>
229-
))}
216+
{filteredCatalogItems.map((catalogItem) => {
217+
const expectedProperties = catalogItem.config?.map((c: { name: string, parameters: ParsedParameters }) => Object.keys(c.parameters)).flat() || []
218+
const hasAllConfig = !expectedProperties.length || expectedProperties.every((p: string) => config[catalogItem.name]?.[p])
219+
return (
220+
<Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={catalogItem.name}>
221+
<CatalogItemCard
222+
hasAllConfig={hasAllConfig}
223+
setConfiguringItem={setConfiguringItem}
224+
openUrl={() => {
225+
client.host.openExternal(Ref.fromRef(catalogItem.ref).toURL(true));
226+
}}
227+
item={catalogItem}
228+
ddVersion={ddVersion}
229+
canRegister={canRegister}
230+
registered={Object.keys(registryItems).some((i) => i === catalogItem.name)}
231+
register={registerCatalogItem}
232+
unregister={unregisterCatalogItem}
233+
onSecretChange={async (secret) => {
234+
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
235+
loadSecrets();
236+
}}
237+
secrets={secrets}
238+
/>
239+
</Grid2>
240+
)
241+
})}
230242
<Grid2 size={12}>
231243
<Card sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
232244
<CardContent>
@@ -240,16 +252,19 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
240252
</Grid2>
241253
</Grid2>}
242254
{tab === 1 && <Grid2 container spacing={1} width='90vw' maxWidth={1000}>
243-
{Object.entries(registryItems).map(([name, item]) => (
244-
name.toLowerCase().includes(search.toLowerCase()) && <Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={name}>
245-
<CatalogItemCard ddVersion={ddVersion} item={catalogItems.find((i) => i.name === name)!} openUrl={() => {
246-
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
247-
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} onSecretChange={async (secret) => {
248-
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
249-
loadSecrets();
250-
}} secrets={secrets} setConfiguringItem={setConfiguringItem} />
251-
</Grid2>
252-
))}
255+
{Object.entries(registryItems).map(([name, item]) => {
256+
const hasAllConfig = item.config?.map((c: any) => c.name).every((c: any) => config[name]?.[c])
257+
return (
258+
name.toLowerCase().includes(search.toLowerCase()) && <Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={name}>
259+
<CatalogItemCard hasAllConfig={hasAllConfig} ddVersion={ddVersion} item={catalogItems.find((i) => i.name === name)!} openUrl={() => {
260+
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
261+
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} onSecretChange={async (secret) => {
262+
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
263+
loadSecrets();
264+
}} secrets={secrets} setConfiguringItem={setConfiguringItem} />
265+
</Grid2>
266+
)
267+
})}
253268
</Grid2>}
254269
{tab === 2 && ddVersion && <SecretList secrets={secrets} ddVersion={ddVersion} />}
255270
</Stack >

0 commit comments

Comments
 (0)