Skip to content

Commit 3de9d93

Browse files
author
colinmcneil
committed
Implement basic secret management
1 parent d9f40aa commit 3de9d93

File tree

5 files changed

+59
-16
lines changed

5 files changed

+59
-16
lines changed

src/extension/ui/src/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { v1 } from "@docker/extension-api-client-types";
22
import { getUser, readFileInPromptsVolume } from "./FileWatcher";
33

44
export const POLL_INTERVAL = 1000 * 30;
5+
export const MCP_POLICY_NAME = 'MCP=*';
56
export const CATALOG_URL = 'https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/prompts/catalog.yaml'
67
export const DOCKER_MCP_CONFIG = {
78
"command": "docker",

src/extension/ui/src/Secrets.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ namespace Secrets {
2525
return JSON.parse(response?.stdout || '[]');
2626
}
2727

28+
2829
export async function addSecret(client: v1.DockerDesktopClient, secret: Secret): Promise<void> {
29-
const response = await client.extension.host?.cli.exec('host-binary', ['add', secret.name, secret.value]);
30-
console.log(response);
30+
try {
31+
await client.extension.host?.cli.exec('host-binary', ['--name', secret.name, '--value', secret.value]);
32+
client.desktopUI.toast.success('Secret set successfully')
33+
} catch (error) {
34+
client.desktopUI.toast.error('Failed to set secret: ' + error)
35+
}
3136
}
3237

3338
// Get all relevant secrets for a given set of catalog items

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { parse, stringify } from 'yaml';
88
import { getRegistry } from '../Registry';
99
import { FolderOpenRounded, Search, Settings } from '@mui/icons-material';
1010
import { tryRunImageSync } from '../FileWatcher';
11-
import { CATALOG_URL, POLL_INTERVAL } from '../Constants';
11+
import { CATALOG_URL, MCP_POLICY_NAME, POLL_INTERVAL } from '../Constants';
1212
import { SecretList } from './SecretList';
1313
import Secrets from '../Secrets';
1414

@@ -26,6 +26,14 @@ const filterCatalog = (catalogItems: CatalogItemWithName[], registryItems: { [ke
2626

2727
const NEVER_SHOW_AGAIN_KEY = 'registry-sync-never-show-again';
2828

29+
const debounce = (func: (...args: any[]) => void, delay: number) => {
30+
let timeout: NodeJS.Timeout;
31+
return (...args: any[]) => {
32+
clearTimeout(timeout);
33+
timeout = setTimeout(() => func(...args), delay);
34+
};
35+
}
36+
2937
export const CatalogGrid: React.FC<CatalogGridProps> = ({
3038
registryItems,
3139
canRegister,
@@ -70,10 +78,14 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
7078

7179
const loadSecrets = async () => {
7280
const response = await Secrets.getSecrets(client);
73-
console.log(response);
74-
setSecrets(response);
81+
setSecrets(response || []);
7582
}
7683

84+
const debouncedAddSecret = debounce((client: v1.DockerDesktopClient, name: string, value: string) => {
85+
Secrets.addSecret(client, { name, value, policies: [MCP_POLICY_NAME] })
86+
loadSecrets();
87+
}, 1000);
88+
7789
const registerCatalogItem = async (item: CatalogItemWithName) => {
7890
try {
7991
const currentRegistry = await getRegistry(client);
@@ -193,6 +205,10 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
193205
registered={Object.keys(registryItems).some((i) => i === item.name)}
194206
register={registerCatalogItem}
195207
unregister={unregisterCatalogItem}
208+
onSecretChange={(secret) => {
209+
debouncedAddSecret(client, secret.name, secret.value);
210+
}}
211+
secrets={secrets}
196212
/>
197213
</Grid2>
198214
))}
@@ -213,7 +229,9 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
213229
name.toLowerCase().includes(search.toLowerCase()) && <Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={name}>
214230
<CatalogItemCard item={catalogItems.find((i) => i.name === name)!} openUrl={() => {
215231
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
216-
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} />
232+
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} onSecretChange={(secret) => {
233+
debouncedAddSecret(client, secret.name, secret.value);
234+
}} secrets={secrets} />
217235
</Grid2>
218236
))}
219237
</Grid2>}

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

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { Badge, CircularProgress, Dialog, DialogContent, DialogTitle, Divider, I
22
import Button from '@mui/material/Button';
33
import { Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material";
44
import { Ref } from "../Refs";
5-
import { useState } from "react";
5+
import { useEffect, useState } from "react";
66
import { trackEvent } from "../Usage";
7-
import { Article, AttachFile, Build, CheckBox, LockRounded } from "@mui/icons-material";
7+
import { Article, AttachFile, Build, CheckBox, Delete, LockReset, LockRounded, Save } from "@mui/icons-material";
8+
import Secrets from "../Secrets";
89

910
const iconSize = 16
1011

@@ -22,10 +23,20 @@ export interface CatalogItemWithName extends CatalogItem {
2223
name: string;
2324
}
2425

25-
export function CatalogItemCard({ openUrl, item, canRegister, registered, register, unregister }: { openUrl: () => void, item: CatalogItemWithName, canRegister: boolean, registered: boolean, register: (item: CatalogItemWithName) => Promise<void>, unregister: (item: CatalogItemWithName) => Promise<void> }) {
26+
export function CatalogItemCard({ openUrl, item, canRegister, registered, register, unregister, onSecretChange, secrets }: { openUrl: () => void, item: CatalogItemWithName, canRegister: boolean, registered: boolean, register: (item: CatalogItemWithName) => Promise<void>, unregister: (item: CatalogItemWithName) => Promise<void>, onSecretChange: (secret: { name: string, value: string }) => void, secrets: Secrets.Secret[] }) {
27+
const loadAssignedSecrets = () => {
28+
const assignedSecrets = Secrets.getAssignedSecrets(item, secrets);
29+
setAssignedSecrets(assignedSecrets)
30+
}
2631
const [isRegistering, setIsRegistering] = useState(false)
2732
const [showSecretDialog, setShowSecretDialog] = useState(false)
28-
const [secrets, setSecrets] = useState<{ name: string, value: string }[]>(item.secrets?.map(secret => ({ name: secret.name, value: '' })) || [])
33+
const [assignedSecrets, setAssignedSecrets] = useState<{ name: string, assigned: boolean }[]>([])
34+
const [changedSecrets, setChangedSecrets] = useState<{ [key: string]: string | undefined }>({})
35+
36+
useEffect(() => {
37+
loadAssignedSecrets()
38+
}, [secrets])
39+
2940
return (
3041
<>
3142
<Dialog open={showSecretDialog} onClose={() => setShowSecretDialog(false)}>
@@ -36,8 +47,16 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
3647
</DialogTitle>
3748
<DialogContent>
3849
<Stack direction="column" spacing={2}>
39-
{item.secrets?.map(secret => (
40-
<TextField type="password" key={secret.name} label={secret.name} value={secrets.find(s => s.name === secret.name)?.value || ''} onChange={(event) => setSecrets(secrets.map(s => s.name === secret.name ? { ...s, value: event.target.value } : s))} />
50+
{assignedSecrets?.map(secret => (
51+
<Stack key={secret.name} direction="row" spacing={2} alignItems="center">
52+
<TextField placeholder={assignedSecrets.find(s => s.name === secret.name)?.assigned ? '********' : 'Enter secret value'} type="password" key={secret.name} label={secret.name} value={changedSecrets[secret.name] || ''} onChange={(event) => setChangedSecrets({ ...changedSecrets, [secret.name]: event.target.value })} />
53+
{assignedSecrets.find(s => s.name === secret.name)?.assigned && changedSecrets[secret.name] && <IconButton onClick={() => setChangedSecrets({ ...changedSecrets, [secret.name]: undefined })}>
54+
<LockReset />
55+
</IconButton>}
56+
{changedSecrets[secret.name] && <IconButton onClick={() => onSecretChange({ name: secret.name, value: changedSecrets[secret.name] || '' })}>
57+
<Save />
58+
</IconButton>}
59+
</Stack>
4160
))}
4261
</Stack>
4362
</DialogContent>
@@ -97,7 +116,7 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
97116
</Stack>
98117
}>
99118
<IconButton onClick={() => setShowSecretDialog(!showSecretDialog)}>
100-
<Badge badgeContent={item.secrets?.length || "0"} color="warning">
119+
<Badge badgeContent={item.secrets?.length || "0"} color={assignedSecrets?.every(s => s.assigned) ? 'success' : 'warning'}>
101120
<LockRounded sx={{ fontSize: iconSize }} />
102121
</Badge>
103122
</IconButton>

src/extension/ui/src/components/SecretList.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Secret list for the tab
22

3-
import { List, ListItem, ListItemText } from "@mui/material";
3+
import { List, ListItem, ListItemText, Typography } from "@mui/material";
44
import Secrets from "../Secrets";
55

66
export const SecretList = ({ secrets }: { secrets: Secrets.Secret[] }) => {
7-
return <List>
7+
return <List subheader={<Typography variant="h2">The following secrets are available to use in your prompts:</Typography>} sx={{ fontSize: '1.2rem' }}>
88
{secrets.map((secret) => (
99
<ListItem key={secret.name}>
10-
<ListItemText primary={secret.name} />
10+
<ListItemText primary={<Typography variant="h6">{secret.name}</Typography>} />
1111
</ListItem>
1212
))}
1313
</List>

0 commit comments

Comments
 (0)