Skip to content

Commit

Permalink
Implement basic secret management
Browse files Browse the repository at this point in the history
  • Loading branch information
ColinMcNeil committed Mar 3, 2025
1 parent d9f40aa commit 3de9d93
Showing 5 changed files with 59 additions and 16 deletions.
1 change: 1 addition & 0 deletions src/extension/ui/src/Constants.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { v1 } from "@docker/extension-api-client-types";
import { getUser, readFileInPromptsVolume } from "./FileWatcher";

export const POLL_INTERVAL = 1000 * 30;
export const MCP_POLICY_NAME = 'MCP=*';
export const CATALOG_URL = 'https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/prompts/catalog.yaml'
export const DOCKER_MCP_CONFIG = {
"command": "docker",
9 changes: 7 additions & 2 deletions src/extension/ui/src/Secrets.ts
Original file line number Diff line number Diff line change
@@ -25,9 +25,14 @@ namespace Secrets {
return JSON.parse(response?.stdout || '[]');
}


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

// Get all relevant secrets for a given set of catalog items
26 changes: 22 additions & 4 deletions src/extension/ui/src/components/CatalogGrid.tsx
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import { parse, stringify } from 'yaml';
import { getRegistry } from '../Registry';
import { FolderOpenRounded, Search, Settings } from '@mui/icons-material';
import { tryRunImageSync } from '../FileWatcher';
import { CATALOG_URL, POLL_INTERVAL } from '../Constants';
import { CATALOG_URL, MCP_POLICY_NAME, POLL_INTERVAL } from '../Constants';
import { SecretList } from './SecretList';
import Secrets from '../Secrets';

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

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

const debounce = (func: (...args: any[]) => void, delay: number) => {
let timeout: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
}

export const CatalogGrid: React.FC<CatalogGridProps> = ({
registryItems,
canRegister,
@@ -70,10 +78,14 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({

const loadSecrets = async () => {
const response = await Secrets.getSecrets(client);
console.log(response);
setSecrets(response);
setSecrets(response || []);
}

const debouncedAddSecret = debounce((client: v1.DockerDesktopClient, name: string, value: string) => {
Secrets.addSecret(client, { name, value, policies: [MCP_POLICY_NAME] })
loadSecrets();
}, 1000);

const registerCatalogItem = async (item: CatalogItemWithName) => {
try {
const currentRegistry = await getRegistry(client);
@@ -193,6 +205,10 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
registered={Object.keys(registryItems).some((i) => i === item.name)}
register={registerCatalogItem}
unregister={unregisterCatalogItem}
onSecretChange={(secret) => {
debouncedAddSecret(client, secret.name, secret.value);
}}
secrets={secrets}
/>
</Grid2>
))}
@@ -213,7 +229,9 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
name.toLowerCase().includes(search.toLowerCase()) && <Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={name}>
<CatalogItemCard item={catalogItems.find((i) => i.name === name)!} openUrl={() => {
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} />
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} onSecretChange={(secret) => {
debouncedAddSecret(client, secret.name, secret.value);
}} secrets={secrets} />
</Grid2>
))}
</Grid2>}
33 changes: 26 additions & 7 deletions src/extension/ui/src/components/PromptCard.tsx
Original file line number Diff line number Diff line change
@@ -2,9 +2,10 @@ import { Badge, CircularProgress, Dialog, DialogContent, DialogTitle, Divider, I
import Button from '@mui/material/Button';
import { Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material";
import { Ref } from "../Refs";
import { useState } from "react";
import { useEffect, useState } from "react";
import { trackEvent } from "../Usage";
import { Article, AttachFile, Build, CheckBox, LockRounded } from "@mui/icons-material";
import { Article, AttachFile, Build, CheckBox, Delete, LockReset, LockRounded, Save } from "@mui/icons-material";
import Secrets from "../Secrets";

const iconSize = 16

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

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> }) {
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[] }) {
const loadAssignedSecrets = () => {
const assignedSecrets = Secrets.getAssignedSecrets(item, secrets);
setAssignedSecrets(assignedSecrets)
}
const [isRegistering, setIsRegistering] = useState(false)
const [showSecretDialog, setShowSecretDialog] = useState(false)
const [secrets, setSecrets] = useState<{ name: string, value: string }[]>(item.secrets?.map(secret => ({ name: secret.name, value: '' })) || [])
const [assignedSecrets, setAssignedSecrets] = useState<{ name: string, assigned: boolean }[]>([])
const [changedSecrets, setChangedSecrets] = useState<{ [key: string]: string | undefined }>({})

useEffect(() => {
loadAssignedSecrets()
}, [secrets])

return (
<>
<Dialog open={showSecretDialog} onClose={() => setShowSecretDialog(false)}>
@@ -36,8 +47,16 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
</DialogTitle>
<DialogContent>
<Stack direction="column" spacing={2}>
{item.secrets?.map(secret => (
<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))} />
{assignedSecrets?.map(secret => (
<Stack key={secret.name} direction="row" spacing={2} alignItems="center">
<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 })} />
{assignedSecrets.find(s => s.name === secret.name)?.assigned && changedSecrets[secret.name] && <IconButton onClick={() => setChangedSecrets({ ...changedSecrets, [secret.name]: undefined })}>
<LockReset />
</IconButton>}
{changedSecrets[secret.name] && <IconButton onClick={() => onSecretChange({ name: secret.name, value: changedSecrets[secret.name] || '' })}>
<Save />
</IconButton>}
</Stack>
))}
</Stack>
</DialogContent>
@@ -97,7 +116,7 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
</Stack>
}>
<IconButton onClick={() => setShowSecretDialog(!showSecretDialog)}>
<Badge badgeContent={item.secrets?.length || "0"} color="warning">
<Badge badgeContent={item.secrets?.length || "0"} color={assignedSecrets?.every(s => s.assigned) ? 'success' : 'warning'}>
<LockRounded sx={{ fontSize: iconSize }} />
</Badge>
</IconButton>
6 changes: 3 additions & 3 deletions src/extension/ui/src/components/SecretList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Secret list for the tab

import { List, ListItem, ListItemText } from "@mui/material";
import { List, ListItem, ListItemText, Typography } from "@mui/material";
import Secrets from "../Secrets";

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

0 comments on commit 3de9d93

Please sign in to comment.