Skip to content

Commit 9c6f0df

Browse files
author
colinmcneil
committed
Add UI basics
1 parent d9e36fc commit 9c6f0df

File tree

9 files changed

+70
-100
lines changed

9 files changed

+70
-100
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { Box, Chip, Stack, Typography } from "@mui/material";
1+
import { Chip, Stack } from "@mui/material";
22
import { CatalogItem } from "../../types/catalog";
3-
import { Hardware, WarningAmberOutlined } from "@mui/icons-material";
3+
import { Hardware } from "@mui/icons-material";
44

55
type BottomProps = {
66
item: CatalogItem,
77
needsConfiguration: boolean
88
}
99

10-
const Bottom = ({ item, needsConfiguration }: BottomProps) => {
10+
const Bottom = ({ item }: BottomProps) => {
1111
return (
1212
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
1313
{<Chip sx={{ fontSize: '1.2em', p: '4px 8px' }} label={

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

Lines changed: 16 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -27,81 +27,35 @@ const LoadingState = () => {
2727
}
2828

2929
const ConfigEditor = ({ catalogItem, client }: { catalogItem: CatalogItemRichened, client: v1.DockerDesktopClient }) => {
30-
const configSchema = catalogItem.config;
30+
const configSchema = catalogItem.configSchema;
3131

32-
const { config: existingConfig, saveConfig: updateExistingConfig, configLoading, tryLoadConfig } = useConfig(client);
32+
const { config, saveConfig: updateExistingConfig, configLoading } = useConfig(client);
3333

34-
const existingConfigForItem = existingConfig?.[catalogItem.name];
34+
const existingConfigForItem = catalogItem.configValue || {};
3535

36-
// Create a key that changes whenever existingConfigForItem changes
37-
const configKey = useMemo(() =>
38-
existingConfigForItem ? JSON.stringify(existingConfigForItem) : 'empty-config',
39-
[existingConfigForItem]);
4036

4137
const [localConfig, setLocalConfig] = useState<{ [key: string]: any } | undefined>(undefined);
42-
const [config, setConfig] = useState<any>(undefined);
4338
const [savingKeys, setSavingKeys] = useState<Set<string>>(new Set());
4439

4540
// Use memoized flattenedConfig to ensure it only updates when config changes
4641
// This MUST be called before any early returns to avoid conditional hook calls
4742
const flattenedConfig = useMemo(() =>
48-
config ? deepFlattenObject(config) : {},
49-
[config]);
43+
configSchema ? deepFlattenObject({ ...catalogItem.configTemplate, ...existingConfigForItem }) : {},
44+
[catalogItem.configTemplate, existingConfigForItem, configSchema]);
5045

5146
// Reset local config when the existing config changes
5247
useEffect(() => {
5348
if (!configSchema) return;
54-
55-
try {
56-
const template = getTemplateForItem(catalogItem, existingConfigForItem);
57-
setConfig(template);
58-
setLocalConfig(deepFlattenObject(template));
59-
} catch (error) {
60-
console.error("Error processing config schema:", error);
61-
}
62-
}, [existingConfig, existingConfigForItem, configSchema, configKey]);
63-
64-
// Handle saving a config item
65-
const handleSaveConfig = async (key: string) => {
66-
if (savingKeys.has(key)) return;
67-
68-
try {
69-
setSavingKeys(prev => new Set([...prev, key]));
70-
const updatedConfig = deepSet(existingConfigForItem || {}, key, localConfig?.[key]);
71-
72-
// Force a deep clone to ensure we're sending a new object reference
73-
const cleanConfig = JSON.parse(JSON.stringify(updatedConfig));
74-
75-
// Wait for the config update to complete
76-
await updateExistingConfig(catalogItem.name, cleanConfig);
77-
78-
// Also update our local state to match the new saved state
79-
const schema = new JsonSchema.Draft2019(configSchema[0]);
80-
const newTemplate = schema.getTemplate(cleanConfig);
81-
setConfig(newTemplate);
82-
83-
// Reset localConfig to match the new template
84-
setLocalConfig(deepFlattenObject(newTemplate));
85-
86-
// After saving, force a refetch to ensure UI is in sync
87-
await tryLoadConfig();
88-
} catch (error) {
89-
console.error("Error saving config:", error);
90-
} finally {
91-
setSavingKeys(prev => {
92-
const updated = new Set([...prev]);
93-
updated.delete(key);
94-
return updated;
95-
});
96-
}
97-
};
49+
if (localConfig) return;
50+
setLocalConfig(flattenedConfig);
51+
}, [flattenedConfig]);
9852

9953
// Early returns
10054
if (!configSchema) {
10155
return <EmptyState />;
10256
}
10357

104-
if (!existingConfig && !configLoading) {
58+
if (!config && !configLoading) {
10559
return <EmptyState />;
10660
}
10761

@@ -133,22 +87,15 @@ const ConfigEditor = ({ catalogItem, client }: { catalogItem: CatalogItemRichene
13387
{isSaving ? (
13488
<CircularProgress size={24} />
13589
) : (
136-
<IconButton onClick={() => handleSaveConfig(key)}>
137-
<CheckOutlined sx={{ color: 'success.main' }} />
90+
<IconButton onClick={() => setLocalConfig({
91+
...localConfig,
92+
[key]: flattenedConfig[key]
93+
})}
94+
disabled={isSaving}
95+
>
96+
<CloseOutlined sx={{ color: 'error.main' }} />
13897
</IconButton>
13998
)}
140-
<IconButton
141-
onClick={() => {
142-
// Reset this field to match the original config
143-
setLocalConfig({
144-
...localConfig,
145-
[key]: flattenedConfig[key]
146-
});
147-
}}
148-
disabled={isSaving}
149-
>
150-
<CloseOutlined sx={{ color: 'error.main' }} />
151-
</IconButton>
15299
</Stack>}
153100
</Stack>
154101
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const Tile = ({ item, client, unAssignedConfig }: TileProps) => {
9494
} else {
9595
unregisterCatalogItem(item)
9696
}
97-
}} item={item} unAssignedConfig={unAssignedConfig} unAssignedSecrets={unAssignedSecrets} registered={item.registered} />
97+
}} item={item} unAssignedConfig={unAssignedConfig} unAssignedSecrets={unAssignedSecrets} />
9898
<Center item={item} />
9999
<Divider sx={{ marginBottom: 1 }} />
100100
<Bottom item={item} needsConfiguration={Boolean(unAssignedSecrets.length || unAssignedConfig.length)} />

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,26 @@ const ConfigurationModal = ({
5252
catalogItem,
5353
client,
5454
}: ConfigurationModalProps) => {
55-
const [localSecrets, setLocalSecrets] = useState<{ [key: string]: string | undefined }>({});
55+
const [localSecrets, setLocalSecrets] = useState<{ [key: string]: string | undefined } | undefined>(undefined);
5656
const theme = useTheme();
5757

5858
const { isLoading: secretsLoading, mutate: mutateSecret } = useSecrets(client)
5959
const { registryLoading } = useRegistry(client)
6060
const { registerCatalogItem, unregisterCatalogItem } = useCatalogOperations(client)
6161
const { configLoading } = useConfig(client)
6262

63+
useEffect(() => {
64+
if (localSecrets) return;
65+
setLocalSecrets(catalogItem.secrets.reduce((acc, secret) => {
66+
acc[secret.name] = secret.assigned ? ASSIGNED_SECRET_PLACEHOLDER : '';
67+
return acc;
68+
}, {} as { [key: string]: string | undefined }));
69+
}, [catalogItem.secrets]);
70+
71+
if (catalogItem.name === 'atlassian') {
72+
console.log(localSecrets)
73+
}
74+
6375
const toolChipStyle = {
6476
padding: '2px 8px',
6577
justifyContent: 'center',
@@ -80,25 +92,28 @@ const ConfigurationModal = ({
8092
// State for tabs
8193
const [tabValue, setTabValue] = useState(0);
8294

83-
// State for secrets
84-
const [assignedSecrets, setAssignedSecrets] = useState<{ name: string, assigned: boolean }[]>([]);
85-
8695
// Handle tab change
8796
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
8897
setTabValue(newValue);
8998
};
9099

91-
const contributesNoConfigOrSecrets = (!catalogItem.config || catalogItem.config.length === 0) && (!catalogItem.secrets || catalogItem.secrets.length === 0);
100+
const contributesNoConfigOrSecrets = (!catalogItem.configSchema || catalogItem.configSchema.length === 0) && (!catalogItem.secrets || catalogItem.secrets.length === 0);
92101

93-
if (secretsLoading || registryLoading) {
102+
if (secretsLoading || registryLoading || configLoading) {
94103
return <>
95104
<CircularProgress />
96105
<Typography>Loading registry...</Typography>
97106
</>
98107
}
99108

109+
if (!localSecrets) {
110+
return <>
111+
<CircularProgress />
112+
<Typography>Loading secrets...</Typography>
113+
</>
114+
}
115+
100116
const canRegister = catalogItem.canRegister;
101-
const registered = catalogItem.registered;
102117

103118
return (
104119
<Modal
@@ -178,16 +193,17 @@ const ConfigurationModal = ({
178193
<Typography variant="h6" sx={{ mb: 1 }}>Secrets</Typography>
179194
{
180195
catalogItem.secrets && catalogItem.secrets?.length > 0 ? (
181-
assignedSecrets?.map(secret => {
196+
catalogItem.secrets.map(secret => {
182197
const secretEdited = secret.assigned ? localSecrets[secret.name] !== ASSIGNED_SECRET_PLACEHOLDER : localSecrets[secret.name] !== '';
198+
console.log(secret.name, secretEdited, secret.assigned, localSecrets[secret.name])
183199
return (
184200
<Stack key={secret.name} direction="row" spacing={2} alignItems="center">
185201
<TextField key={secret.name} label={secret.name} value={localSecrets[secret.name]} fullWidth onChange={(e) => {
186202
setLocalSecrets({ ...localSecrets, [secret.name]: e.target.value });
187203
}} type='password' />
188204
{secret.assigned && !secretEdited && <IconButton size="small" color="error" onClick={() => {
189-
setLocalSecrets({ ...localSecrets, [secret.name]: UNASSIGNED_SECRET_PLACEHOLDER });
190-
mutateSecret.mutateAsync({ name: secret.name, value: UNASSIGNED_SECRET_PLACEHOLDER, policies: [MCP_POLICY_NAME] });
205+
setLocalSecrets({ ...localSecrets, [secret.name]: '' });
206+
mutateSecret.mutateAsync({ name: secret.name, value: undefined, policies: [MCP_POLICY_NAME] });
191207
}}>
192208
<DeleteOutlined />
193209
</IconButton>}

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@ type TopProps = {
66
unAssignedConfig: { name: string, assigned: boolean }[],
77
onToggleRegister: (checked: boolean) => void,
88
unAssignedSecrets: { name: string, assigned: boolean }[],
9-
registered: boolean
109
item: CatalogItemRichened
1110
}
1211

13-
export default function Top({ item, onToggleRegister, registered }: TopProps) {
14-
15-
const canRegister = true; // TODO: remove this
12+
export default function Top({ item, onToggleRegister }: TopProps) {
1613

1714
const getActionButton = () => {
18-
if (!canRegister) {
15+
if (!item.canRegister) {
1916
return <Stack direction="row" spacing={0} alignItems="center">
2017
<Tooltip title="This tile needs configuration before it can be used.">
2118
<span>
@@ -25,9 +22,9 @@ export default function Top({ item, onToggleRegister, registered }: TopProps) {
2522
</Stack>
2623
}
2724
return <Stack direction="row" spacing={0} alignItems="center">
28-
<Tooltip title={registered ? "Unregistering this tile will hide it from MCP clients." : "Registering this tile will expose it to MCP clients."}>
25+
<Tooltip title={item.registered ? "Unregistering this tile will hide it from MCP clients." : "Registering this tile will expose it to MCP clients."}>
2926
<Switch
30-
checked={registered}
27+
checked={item.registered}
3128
onChange={(event, checked) => {
3229
event.stopPropagation()
3330
event.preventDefault()

src/extension/ui/src/hooks/useCatalog.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,33 @@ interface QueryContextWithMeta {
2727

2828
export function useCatalog(client: v1.DockerDesktopClient) {
2929
const queryClient = useQueryClient();
30-
const { data: secrets } = useSecrets(client);
31-
const { registryItems } = useRegistry(client);
32-
const { config } = useConfig(client);
30+
const { data: secrets, isLoading: secretsLoading } = useSecrets(client);
31+
const { registryItems, registryLoading } = useRegistry(client);
32+
const { config, configLoading: configLoading } = useConfig(client);
3333

3434
const enrichCatalogItem = (item: CatalogItemWithName): CatalogItemRichened => {
3535
const secretsWithAssignment = Secrets.getSecretsWithAssignment(item, secrets || []);
3636
const itemConfigValue = config?.[item.name] || {};
37-
const unConfigured = Object.keys(itemConfigValue).length === 0;
37+
const unConfigured = Boolean(item.config && Object.keys(itemConfigValue).length === 0);
3838
const missingASecret = secretsWithAssignment.some((secret) => !secret.assigned);
39-
const enrichedItem = {
39+
40+
const enrichedItem: CatalogItemRichened = {
4041
...item,
4142
secrets: secretsWithAssignment,
4243
configValue: itemConfigValue,
4344
configSchema: item.config,
45+
configTemplate: getTemplateForItem(item, itemConfigValue),
46+
missingConfig: unConfigured,
47+
missingSecrets: missingASecret,
4448
registered: !!registryItems?.[item.name],
4549
canRegister: !registryItems?.[item.name] && !missingASecret && !unConfigured,
4650
name: item.name,
4751
};
52+
4853
delete enrichedItem.config;
49-
if (item.name === 'atlassian') {
50-
console.log(enrichedItem);
54+
if (item.name === 'atlassian' || item.name === 'mcp-sqlite') {
55+
console.log(enrichedItem, unConfigured, missingASecret);
56+
console.log('template', getTemplateForItem(item, itemConfigValue));
5157
}
5258
return enrichedItem;
5359
};
@@ -58,6 +64,7 @@ export function useCatalog(client: v1.DockerDesktopClient) {
5864
refetch: refetchCatalog
5965
} = useQuery({
6066
queryKey: ['catalog'],
67+
enabled: !secretsLoading && !registryLoading && !configLoading,
6168
queryFn: async (context) => {
6269
const queryContext = context as unknown as QueryContextWithMeta;
6370
const showNotification = queryContext.meta?.showNotification ?? false;

src/extension/ui/src/hooks/useConfig.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { v1 } from "@docker/extension-api-client-types";
22
import { getStoredConfig, syncRegistryWithConfig } from '../Registry';
33
import { POLL_INTERVAL } from '../Constants';
44
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
5-
import { CatalogItemRichened } from '../types/catalog';
5+
import { CatalogItemRichened, CatalogItemWithName } from '../types/catalog';
66
import * as JsonSchemaLibrary from 'json-schema-library';
77
import { escapeJSONForPlatformShell, tryRunImageSync } from '../FileUtils';
88
import { stringify } from 'yaml';
99
import { useRef } from 'react';
1010

11-
export const getTemplateForItem = (item: CatalogItemRichened, existingConfigForItem: { [key: string]: any } = {}) => {
11+
export const getTemplateForItem = (item: CatalogItemWithName, existingConfigForItem: { [key: string]: any } = {}) => {
1212
const config = item.config;
1313
if (!config) return {};
1414
const schema = new JsonSchemaLibrary.Draft2019(config[0]);

src/extension/ui/src/types/catalog/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ export interface CatalogItemRichened extends CatalogItem {
3030
configSchema: any;
3131
registered: boolean;
3232
canRegister: boolean;
33+
missingConfig: boolean;
34+
missingSecrets: boolean;
35+
configTemplate: { [key: string]: any };
3336
}

src/extension/ui/src/types/secrets/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
export type Secret = {
55
name: string;
6-
value: string;
6+
value: string | undefined;
77
policies: string[];
88
};
99

0 commit comments

Comments
 (0)