Skip to content

Commit 5b5a3a0

Browse files
authored
Fix issues with config & secrets (#113)
* Fix issues with config & secrets Allow deletion of secrets * Update secret form * Add screen shots * Add new screenshots to metadata
1 parent 1d7cb06 commit 5b5a3a0

File tree

10 files changed

+34
-79
lines changed

10 files changed

+34
-79
lines changed

src/extension/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ LABEL org.opencontainers.image.title="AI Tool Catalog" \
2626
org.opencontainers.image.description="AI Tool Catalog" \
2727
org.opencontainers.image.vendor="Docker Inc" \
2828
com.docker.desktop.extension.api.version="0.3.4" \
29-
com.docker.extension.screenshots='[{"alt":"screenshot of the extension UI", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/docs/assets/img/dd-ext-screenshot.png"}, {"alt":"Screenshot of MCP Client configuration", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/docs/assets/img/dd-extension-screenshot-2.png"}]' \
29+
com.docker.extension.screenshots='[{"alt":"screenshot of the extension UI", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/src/extension/ui/src/assets/screenshot1.png"}, {"alt":"Screenshot of MCP Client configuration", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/src/extension/ui/src/assets/screenshot2.png"}]' \
3030
com.docker.desktop.extension.icon="https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/src/extension/docker.svg" \
3131
com.docker.extension.detailed-description="A catalog of Dockerized MCP tools for developers" \
3232
com.docker.extension.publisher-url="https://www.docker.com/" \

src/extension/host-binary/cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ func DeleteSecret(ctx context.Context) *cobra.Command {
9494
return runDeleteSecret(ctx, *opts)
9595
},
9696
}
97+
flags := cmd.Flags()
98+
flags.StringVarP(&opts.Name, "name", "n", "", "Name of the secret")
99+
_ = cmd.MarkFlagRequired("name")
97100
return cmd
98101
}
99102

src/extension/ui/src/Constants.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ export const TILE_DESCRIPTION_MAX_LENGTH = 180;
1515
export const CATALOG_LAYOUT_SX = {
1616
width: '90vw',
1717
maxWidth: '1200px',
18-
}
18+
}
19+
20+
21+
export const ASSIGNED_SECRET_PLACEHOLDER = '********';
22+
export const UNASSIGNED_SECRET_PLACEHOLDER = 'UNASSIGNED';

src/extension/ui/src/Registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const syncConfigWithRegistry = async (client: v1.DockerDesktopClient, reg
7979
const configInConfigFile = config[registryItemName]
8080
if (configInConfigFile) {
8181
const mergedConfig = mergeDeep(configInConfigFile, configInRegistry)
82-
config[registryItemName] = mergedConfig
82+
config[registryItemName][registryItemName] = mergedConfig
8383
}
8484
}
8585
const newConfigString = JSON.stringify({ config })

src/extension/ui/src/Secrets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ namespace Secrets {
3737

3838
export async function deleteSecret(client: v1.DockerDesktopClient, name: string): Promise<void> {
3939
try {
40-
const response = await client.extension.host?.cli.exec('host-binary', ['delete', '--name', name]);
40+
const response = await client.extension.host?.cli.exec('host-binary', ['delete', name]);
4141
client.desktopUI.toast.success('Secret deleted successfully')
4242
if (!response) {
4343
client.desktopUI.toast.error('Failed to delete secret. Could not get response from host-binary.')
473 KB
Loading
136 KB
Loading

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
4040
canRegister,
4141
registerCatalogItem,
4242
unregisterCatalogItem,
43-
tryLoadSecrets,
43+
tryUpdateSecrets,
4444
secrets
4545
} = useCatalogContext();
4646

@@ -222,7 +222,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
222222
canRegister={canRegister}
223223
register={registerCatalogItem}
224224
unregister={unregisterCatalogItem}
225-
onSecretChange={tryLoadSecrets}
225+
onSecretChange={tryUpdateSecrets}
226226
secrets={secrets}
227227
setConfiguringItem={setConfiguringItem}
228228
config={config || {}}

src/extension/ui/src/components/tile/Modal.tsx

Lines changed: 7 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { deepFlattenObject, deepGet, deepSet, mergeDeep } from "../../MergeDeep"
1010
import { DeepObject } from "../../types/utils";
1111
import { Parameter, ParameterArray, ParameterObject, Parameters, ParsedParameters, Config } from "../../types/config";
1212
import { Ref } from "../../Refs";
13-
import { CATALOG_LAYOUT_SX } from "../../Constants";
13+
import { ASSIGNED_SECRET_PLACEHOLDER, CATALOG_LAYOUT_SX, UNASSIGNED_SECRET_PLACEHOLDER } from "../../Constants";
1414
import JsonSchemaLibrary from "json-schema-library";
1515
import ConfigEditor from "./ConfigEditor";
1616

@@ -57,8 +57,6 @@ interface ConfigurationModalProps {
5757
onSecretChange: (secret: { name: string, value: string }) => Promise<void>;
5858
}
5959

60-
const ASSIGNED_SECRET_PLACEHOLDER = '********';
61-
const UNASSIGNED_SECRET_PLACEHOLDER = '';
6260

6361
const ConfigurationModal = ({
6462
open,
@@ -71,7 +69,7 @@ const ConfigurationModal = ({
7169
}: ConfigurationModalProps) => {
7270

7371
const { startPull, registryItems, secrets } = useCatalogContext();
74-
const { config, configLoading, saveConfig } = useConfigContext();
72+
const { config, configLoading } = useConfigContext();
7573
const [localSecrets, setLocalSecrets] = useState<{ [key: string]: string | undefined }>({});
7674
const [localConfig, setLocalConfig] = useState<{ [key: string]: any }>({});
7775
const [configTemplate, setConfigTemplate] = useState<Record<string, any>>({});
@@ -190,7 +188,7 @@ const ConfigurationModal = ({
190188
const loadedSecrets = Secrets.getAssignedSecrets(catalogItem, secrets);
191189
setAssignedSecrets(loadedSecrets);
192190
setLocalSecrets(loadedSecrets.reduce((acc, secret) => {
193-
acc[secret.name] = secret.assigned ? ASSIGNED_SECRET_PLACEHOLDER : UNASSIGNED_SECRET_PLACEHOLDER;
191+
acc[secret.name] = secret.assigned ? ASSIGNED_SECRET_PLACEHOLDER : '';
194192
return acc;
195193
}, {} as { [key: string]: string | undefined }));
196194
}, [catalogItem, secrets]);
@@ -257,65 +255,6 @@ const ConfigurationModal = ({
257255
return '';
258256
};
259257

260-
// Validate the entire configuration against schema conditions
261-
const validateFullSchema = (config: { [key: string]: any }, schema: any): { [key: string]: string } => {
262-
const errors: { [key: string]: string } = {};
263-
264-
// Check basic parameter validations
265-
if (schema && schema.parameters) {
266-
Object.entries(schema.parameters).forEach(([key, paramSchema]: [string, any]) => {
267-
if (config[key]) {
268-
if (paramSchema.type === 'object' && paramSchema.properties) {
269-
Object.entries(paramSchema.properties).forEach(([propKey, propSchema]: [string, any]) => {
270-
const propPath = `${key}.${propKey}`;
271-
const propValue = deepGet(config, propPath);
272-
273-
if (propValue !== undefined) {
274-
const error = validateConfigValue(propKey, propValue, propSchema);
275-
if (error) {
276-
errors[propPath] = error;
277-
}
278-
}
279-
});
280-
} else {
281-
const error = validateConfigValue(key, config[key], paramSchema);
282-
if (error) {
283-
errors[key] = error;
284-
}
285-
}
286-
}
287-
});
288-
}
289-
290-
// Check anyOf condition validations
291-
if (schema && schema.anyOf) {
292-
let anyConditionMet = false;
293-
294-
schema.anyOf.forEach((condition: any) => {
295-
if (condition.required) {
296-
const allRequiredPresent = condition.required.every((requiredField: string) =>
297-
config[requiredField] && Object.keys(config[requiredField]).length > 0
298-
);
299-
300-
if (allRequiredPresent) {
301-
anyConditionMet = true;
302-
}
303-
}
304-
});
305-
306-
if (!anyConditionMet) {
307-
const requiredOptions = schema.anyOf
308-
.map((condition: any) => condition.required?.join(' or '))
309-
.filter(Boolean)
310-
.join(' or ');
311-
312-
errors['_schema'] = `At least one of the following must be configured: ${requiredOptions}`;
313-
}
314-
}
315-
316-
return errors;
317-
};
318-
319258
// Determine if we should show the secrets tab and config tab
320259
const hasSecrets = assignedSecrets.length > 0;
321260
const hasConfig = catalogItem.config && catalogItem.config.length > 0;
@@ -408,14 +347,15 @@ const ConfigurationModal = ({
408347
<ConfigEditor catalogItem={catalogItem} />
409348
<Typography variant="h6" sx={{ mb: 1 }}>Secrets</Typography>
410349
{assignedSecrets?.map(secret => {
411-
const secretEdited = secret.assigned ? localSecrets[secret.name] !== ASSIGNED_SECRET_PLACEHOLDER : localSecrets[secret.name] !== UNASSIGNED_SECRET_PLACEHOLDER;
350+
const secretEdited = secret.assigned ? localSecrets[secret.name] !== ASSIGNED_SECRET_PLACEHOLDER : localSecrets[secret.name] !== '';
412351
return (
413352
<Stack key={secret.name} direction="row" spacing={2} alignItems="center">
414353
<TextField key={secret.name} label={secret.name} value={localSecrets[secret.name]} fullWidth onChange={(e) => {
415354
setLocalSecrets({ ...localSecrets, [secret.name]: e.target.value });
416355
}} type='password' />
417-
{!secretEdited && <IconButton size="small" color="error" onClick={() => {
356+
{secret.assigned && !secretEdited && <IconButton size="small" color="error" onClick={() => {
418357
setLocalSecrets({ ...localSecrets, [secret.name]: UNASSIGNED_SECRET_PLACEHOLDER });
358+
onSecretChange({ name: secret.name, value: UNASSIGNED_SECRET_PLACEHOLDER });
419359
}}>
420360
<DeleteOutlined />
421361
</IconButton>}
@@ -426,7 +366,7 @@ const ConfigurationModal = ({
426366
<CheckOutlined sx={{ color: 'success.main' }} />
427367
</IconButton>
428368
<IconButton onClick={async () => {
429-
setLocalSecrets({ ...localSecrets, [secret.name]: secret.assigned ? UNASSIGNED_SECRET_PLACEHOLDER : ASSIGNED_SECRET_PLACEHOLDER });
369+
setLocalSecrets({ ...localSecrets, [secret.name]: secret.assigned ? ASSIGNED_SECRET_PLACEHOLDER : '' });
430370
}}>
431371
<CloseOutlined sx={{ color: 'error.main' }} />
432372
</IconButton>

src/extension/ui/src/context/CatalogContext.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CatalogItemWithName } from '../types/catalog';
44
import { getRegistry } from '../Registry';
55
import Secrets from '../Secrets';
66
import { parse } from 'yaml';
7-
import { CATALOG_URL, POLL_INTERVAL } from '../Constants';
7+
import { CATALOG_URL, POLL_INTERVAL, UNASSIGNED_SECRET_PLACEHOLDER } from '../Constants';
88
import { escapeJSONForPlatformShell, tryRunImageSync } from '../FileWatcher';
99
import { stringify } from 'yaml';
1010
import { ExecResult } from '@docker/extension-api-client-types/dist/v0';
@@ -233,15 +233,23 @@ export function CatalogProvider({ children, client }: CatalogProviderProps) {
233233
const updateSecretsMutation = useMutation({
234234
mutationFn: async (secret: { name: string, value: string }) => {
235235
try {
236-
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [] });
237-
return secret;
238-
} catch (error) {
236+
if (secret.value === UNASSIGNED_SECRET_PLACEHOLDER) {
237+
await Secrets.deleteSecret(client, secret.name);
238+
} else {
239+
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [] });
240+
}
241+
}
242+
catch (error) {
239243
client.desktopUI.toast.error('Failed to update secret: ' + error);
240244
throw error;
241245
}
242246
},
243247
onSuccess: () => {
244248
refetchSecrets();
249+
},
250+
onError: (error) => {
251+
client.desktopUI.toast.error('Failed to update secret: ' + error);
252+
throw error;
245253
}
246254
});
247255

@@ -275,7 +283,7 @@ export function CatalogProvider({ children, client }: CatalogProviderProps) {
275283
}
276284
}
277285

278-
newRegistry[item.name] = { ref: item.ref, config: itemConfig };
286+
newRegistry[item.name] = { ref: item.ref, config: { [item.name]: itemConfig } };
279287
}
280288

281289
const payload = escapeJSONForPlatformShell({
@@ -394,7 +402,7 @@ export function CatalogProvider({ children, client }: CatalogProviderProps) {
394402
// Perform deep comparison to prevent unnecessary syncs
395403
const needsSync = Object.keys(registryItems).some(key => {
396404
return registryItems[key].config &&
397-
(!config[key] || JSON.stringify(registryItems[key].config) !== JSON.stringify(config[key]));
405+
(!config[key] || JSON.stringify(registryItems[key].config[key]) !== JSON.stringify(config[key]));
398406
});
399407

400408
if (needsSync) {

0 commit comments

Comments
 (0)