Skip to content

Commit aae8ce7

Browse files
author
colinmcneil
committed
Implement checks for all required secrets on tiles
1 parent 03ecf01 commit aae8ce7

File tree

5 files changed

+73
-24
lines changed

5 files changed

+73
-24
lines changed

src/extension/ui/src/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { useEffect, useState } from 'react';
2-
import AddIcon from '@mui/icons-material/Add';
32
import { createDockerDesktopClient } from '@docker/extension-api-client';
43
import { Stack, Typography, Button, ButtonGroup, Grid, debounce, Card, CardContent, IconButton, Alert, DialogTitle, Dialog, DialogContent, FormControlLabel, Checkbox, CircularProgress, Paper, DialogActions, Box } from '@mui/material';
54
import { CatalogItemWithName } from './components/PromptCard';

src/extension/ui/src/Constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getUser, readFileInPromptsVolume } from "./FileWatcher";
33

44
export const POLL_INTERVAL = 1000 * 30;
55
export const MCP_POLICY_NAME = 'MCP=*';
6+
export const DD_BUILD_WITH_SECRET_SUPPORT = 184396;
67
export const CATALOG_URL = 'https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/prompts/catalog.yaml'
78
export const DOCKER_MCP_CONFIG = {
89
"command": "docker",
@@ -23,7 +24,9 @@ export type MCPClient = {
2324
disconnect: (client: v1.DockerDesktopClient) => Promise<void>;
2425
validateConfig: (content: string) => boolean;
2526
}
26-
27+
export const getUnsupportedSecretMessage = (ddVersion: { version: string, build: number }) => {
28+
return `Secret support is not available in this version of Docker Desktop. You are on version ${ddVersion.version}, but the minimum required version is 4.40.0.`
29+
}
2730
export const SUPPORTED_MCP_CLIENTS: MCPClient[] = [
2831
{
2932
name: 'Claude Desktop',

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

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useState } from 'react';
2-
import { Card, CardContent, IconButton, Alert, Stack, Button, Typography, Grid2, Select, MenuItem, FormControl, InputLabel, Switch, FormGroup, FormControlLabel, Dialog, DialogTitle, DialogContent, Checkbox, Badge, BadgeProps, Link, TextField, Tabs, Tab, Tooltip, InputAdornment } from '@mui/material';
2+
import { Card, CardContent, IconButton, Alert, Stack, Button, Typography, Grid2, Select, MenuItem, FormControl, InputLabel, Switch, FormGroup, FormControlLabel, Dialog, DialogTitle, DialogContent, Checkbox, Badge, BadgeProps, Link, TextField, Tabs, Tab, Tooltip, InputAdornment, CircularProgress } from '@mui/material';
33
import { CatalogItemWithName, CatalogItemCard, CatalogItem } from './PromptCard';
44
import AddIcon from '@mui/icons-material/Add';
55
import { Ref } from '../Refs';
@@ -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, MCP_POLICY_NAME, POLL_INTERVAL } from '../Constants';
11+
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';
1414

@@ -21,9 +21,17 @@ interface CatalogGridProps {
2121
settingsBadgeProps: BadgeProps;
2222
}
2323

24-
const filterCatalog = (catalogItems: CatalogItemWithName[], registryItems: { [key: string]: { ref: string } }, showRegistered: boolean, showUnregistered: boolean, search: string) =>
25-
catalogItems.filter((item) => (showRegistered || !Object.keys(registryItems).includes(item.name)) && (showUnregistered || Object.keys(registryItems).includes(item.name)) && (item.name.toLowerCase().includes(search.toLowerCase())));
24+
const filterCatalog = (catalogItems: CatalogItemWithName[], registryItems: { [key: string]: { ref: string } }, search: string) =>
25+
catalogItems.filter((item) => item.name.toLowerCase().includes(search.toLowerCase()));
2626

27+
const parseDDVersion = (ddVersion: string) => {
28+
//eg: Docker Desktop 4.40.0 (184396)
29+
const [, , version, build] = ddVersion.split(' ');
30+
return {
31+
version,
32+
build: parseInt(build.replace('(', '').replace(')', ''))
33+
}
34+
}
2735
const NEVER_SHOW_AGAIN_KEY = 'registry-sync-never-show-again';
2836

2937
export const CatalogGrid: React.FC<CatalogGridProps> = ({
@@ -35,14 +43,13 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
3543
settingsBadgeProps
3644
}) => {
3745
const [catalogItems, setCatalogItems] = useState<CatalogItemWithName[]>([]);
38-
const [showRegistered, setShowRegistered] = useState<boolean>(true);
39-
const [showUnregistered, setShowUnregistered] = useState<boolean>(true);
4046
const [showReloadModal, setShowReloadModal] = useState<boolean>(false);
4147
const [search, setSearch] = useState<string>('');
4248
const [tab, setTab] = useState<number>(0);
4349
const [secrets, setSecrets] = useState<Secrets.Secret[]>([]);
50+
const [ddVersion, setDdVersion] = useState<{ version: string, build: number } | null>(null);
4451

45-
const filteredCatalogItems = filterCatalog(catalogItems, registryItems, showRegistered, showUnregistered, search);
52+
const filteredCatalogItems = filterCatalog(catalogItems, registryItems, search);
4653

4754
const loadCatalog = async (showNotification = true) => {
4855
const cachedCatalog = localStorage.getItem('catalog');
@@ -51,7 +58,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
5158
const catalog = await response.text();
5259
const items = parse(catalog)['registry'] as { [key: string]: CatalogItem }
5360
const itemsWithName = Object.entries(items).map(([name, item]) => ({ name, ...item }));
54-
const filteredItems = filterCatalog(itemsWithName, registryItems, showRegistered, showUnregistered, search);
61+
const filteredItems = filterCatalog(itemsWithName, registryItems, search);
5562
setCatalogItems(filteredItems);
5663
localStorage.setItem('catalog', JSON.stringify(filteredItems));
5764
if (showNotification) {
@@ -73,7 +80,12 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
7380
setSecrets(response || []);
7481
}
7582

76-
const registerCatalogItem = async (item: CatalogItemWithName) => {
83+
const loadDDVersion = async () => {
84+
const ddVersionResult = await client.docker.cli.exec('version', ['--format', 'json'])
85+
setDdVersion(parseDDVersion(JSON.parse(ddVersionResult.stdout).Server.Platform.Name));
86+
}
87+
88+
const registerCatalogItem = async (item: CatalogItemWithName, showNotification = true) => {
7789
try {
7890
const currentRegistry = await getRegistry(client);
7991
const newRegistry = { ...currentRegistry, [item.name]: { ref: item.ref } };
@@ -84,12 +96,18 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
8496
}]
8597
})
8698
await tryRunImageSync(client, ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'vonwig/function_write_files:latest', `'${payload}'`])
87-
client.desktopUI.toast.success('Prompt registered successfully. Restart Claude Desktop to apply.');
99+
if (showNotification) {
100+
client.desktopUI.toast.success('Prompt registered successfully. Restart Claude Desktop to apply.');
101+
}
88102
onRegistryChange();
89-
setShowReloadModal(!localStorage.getItem(NEVER_SHOW_AGAIN_KEY));
103+
if (showNotification) {
104+
setShowReloadModal(!localStorage.getItem(NEVER_SHOW_AGAIN_KEY));
105+
}
90106
}
91107
catch (error) {
92-
client.desktopUI.toast.error('Failed to register prompt: ' + error);
108+
if (showNotification) {
109+
client.desktopUI.toast.error('Failed to register prompt: ' + error);
110+
}
93111
}
94112
}
95113

@@ -116,6 +134,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
116134
useEffect(() => {
117135
loadCatalog(false);
118136
loadSecrets();
137+
loadDDVersion();
119138
const interval = setInterval(() => {
120139
loadCatalog(false);
121140
loadSecrets();
@@ -129,6 +148,10 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
129148
catalogItems.some((c) => c.name === i)
130149
)
131150

151+
if (!ddVersion) {
152+
return <CircularProgress />
153+
}
154+
132155

133156
return (
134157
<Stack spacing={2} justifyContent='center' alignItems='center'>
@@ -188,6 +211,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
188211
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
189212
}}
190213
item={item}
214+
ddVersion={ddVersion}
191215
canRegister={canRegister}
192216
registered={Object.keys(registryItems).some((i) => i === item.name)}
193217
register={registerCatalogItem}
@@ -215,7 +239,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
215239
{tab === 1 && <Grid2 container spacing={1} width='90vw' maxWidth={1000}>
216240
{Object.entries(registryItems).map(([name, item]) => (
217241
name.toLowerCase().includes(search.toLowerCase()) && <Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={name}>
218-
<CatalogItemCard item={catalogItems.find((i) => i.name === name)!} openUrl={() => {
242+
<CatalogItemCard ddVersion={ddVersion} item={catalogItems.find((i) => i.name === name)!} openUrl={() => {
219243
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
220244
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} onSecretChange={async (secret) => {
221245
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
@@ -224,7 +248,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
224248
</Grid2>
225249
))}
226250
</Grid2>}
227-
{tab === 2 && <SecretList secrets={secrets} />}
251+
{tab === 2 && ddVersion && <SecretList secrets={secrets} ddVersion={ddVersion} />}
228252
</Stack >
229253
);
230254
};

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
66
import { trackEvent } from "../Usage";
77
import { Article, AttachFile, Build, CheckBox, Delete, LockReset, LockRounded, Save } from "@mui/icons-material";
88
import Secrets from "../Secrets";
9+
import { DD_BUILD_WITH_SECRET_SUPPORT, getUnsupportedSecretMessage } from "../Constants";
910

1011
const iconSize = 16
1112

@@ -23,7 +24,7 @@ export interface CatalogItemWithName extends CatalogItem {
2324
name: string;
2425
}
2526

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 }) => Promise<void>, secrets: Secrets.Secret[] }) {
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 } }) {
2728
const loadAssignedSecrets = () => {
2829
const assignedSecrets = Secrets.getAssignedSecrets(item, secrets);
2930
setAssignedSecrets(assignedSecrets)
@@ -38,6 +39,15 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
3839
loadAssignedSecrets()
3940
}, [secrets])
4041

42+
useEffect(() => {
43+
if (registered && !hasAllSecrets) {
44+
unregister(item, false)
45+
}
46+
}, [registered])
47+
48+
const hasAllSecrets = assignedSecrets.every(s => s.assigned)
49+
const hasDDVersionWithSecretSupport = ddVersion && ddVersion.build >= DD_BUILD_WITH_SECRET_SUPPORT;
50+
4151
return (
4252
<>
4353
<Dialog open={showSecretDialog} onClose={() => setShowSecretDialog(false)}>
@@ -111,7 +121,7 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
111121
<Build sx={{ fontSize: iconSize }} />
112122
</Badge>
113123
</Tooltip>
114-
{item.secrets?.length && (
124+
{item.secrets?.length && (hasDDVersionWithSecretSupport ? (
115125
<Tooltip title={
116126
<Stack sx={{ pr: 1 }} direction="column" spacing={1}>
117127
<List subheader={<Typography sx={{ fontWeight: 'bold' }}>Expected secrets:</Typography>} dense sx={{ p: 0 }}>
@@ -130,13 +140,19 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
130140
</Badge>
131141
</IconButton>
132142
</Tooltip>
133-
)}
143+
) : (
144+
<Tooltip title={getUnsupportedSecretMessage(ddVersion)}>
145+
<IconButton>
146+
<LockRounded sx={{ fontSize: iconSize }} />
147+
</IconButton>
148+
</Tooltip>
149+
))}
134150
</Stack>
135-
<Tooltip title={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."}>
136-
{isRegistering ? <CircularProgress size={20} /> : <Switch
151+
<Tooltip 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."}>
152+
{!hasAllSecrets ? <LockRounded /> : isRegistering ? <CircularProgress size={20} /> : <Switch
137153
size="small"
138154
color={registered ? 'success' : 'primary'}
139-
checked={registered}
155+
checked={registered && hasAllSecrets}
140156
onChange={(event, checked) => {
141157
trackEvent('registry-changed', { name: item.name, ref: item.ref, action: registered ? 'remove' : 'add' });
142158
setIsRegistering(true)

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

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

3-
import { List, ListItem, ListItemText, Typography } from "@mui/material";
3+
import { Alert, List, ListItem, ListItemText, Typography } from "@mui/material";
44
import Secrets from "../Secrets";
5+
import { DD_BUILD_WITH_SECRET_SUPPORT, getUnsupportedSecretMessage } from "../Constants";
6+
7+
export const SecretList = ({ secrets, ddVersion }: { secrets: Secrets.Secret[], ddVersion: { version: string, build: number } }) => {
8+
const hasDDVersionWithSecretSupport = ddVersion && ddVersion.build >= DD_BUILD_WITH_SECRET_SUPPORT;
9+
10+
if (!hasDDVersionWithSecretSupport) {
11+
return <Alert severity="error" sx={{ fontSize: '1.2rem', maxWidth: 600 }}>{getUnsupportedSecretMessage(ddVersion)}</Alert>
12+
}
513

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

0 commit comments

Comments
 (0)