Skip to content

Commit 87917a2

Browse files
authored
Merge pull request #133 from docker/cm/0.2.39
Significant tile toggle check fixes
2 parents d40a929 + 12abafe commit 87917a2

File tree

8 files changed

+106
-115
lines changed

8 files changed

+106
-115
lines changed

src/extension/ui/src/Secrets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ namespace Secrets {
6060
}
6161

6262
// Whether or not each secret has been assigned for a given catalog item
63-
export function getAssignedSecrets(catalogItem: CatalogItemWithName, secrets: Secret[]): { name: string, assigned: boolean }[] {
63+
export function getSecretsWithAssignment(catalogItem: CatalogItemWithName, secrets: Secret[]): { name: string, assigned: boolean }[] {
6464
return catalogItem.secrets?.map((secret) => ({ name: secret.name, assigned: secrets.some((s) => s.name === secret.name) })) || [];
6565
}
6666
}

src/extension/ui/src/components/tabs/ToolCatalog.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ interface ToolCatalogProps {
2626

2727
const ToolCatalog: React.FC<ToolCatalogProps> = ({ config, search, catalogItems, client, onSecretChange, secrets, registryItems, showMine }) => {
2828

29+
const { getCanRegisterCatalogItem } = useCatalogContext();
30+
31+
const tileIsRegistered = (item: CatalogItemWithName) => registryItems[item.name]?.ref !== undefined && getCanRegisterCatalogItem(item);
32+
2933
const filteredCatalogItems = catalogItems.filter(item => {
30-
const isRegistered = registryItems[item.name]?.ref !== undefined;
34+
const isRegistered = tileIsRegistered(item);
3135
const matchesSearch = item.name.toLowerCase().includes(search.toLowerCase());
3236
const hideBecauseItsNotMine = showMine && !isRegistered;
3337
return matchesSearch && !hideBecauseItsNotMine;
@@ -42,7 +46,7 @@ const ToolCatalog: React.FC<ToolCatalogProps> = ({ config, search, catalogItems,
4246
<Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={catalogItem.name}>
4347
<Tile
4448
item={catalogItem}
45-
registered={registryItems[catalogItem.name]?.ref !== undefined}
49+
registered={tileIsRegistered(catalogItem)}
4650
onSecretChange={onSecretChange}
4751
secrets={secrets}
4852
unAssignedConfig={unAssignedConfig}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Alert, Stack } from "@mui/material";
33
import { CatalogItemWithName } from "../../types/catalog";
44
import { useEffect, useState, useCallback, useMemo } from "react";
55
import * as JsonSchema from "json-schema-library";
6-
import { useConfigContext } from "../../context/ConfigContext";
6+
import { getTemplateForItem, useConfigContext } from "../../context/ConfigContext";
77
import { deepFlattenObject, deepSet } from "../../MergeDeep";
88
import { CheckOutlined, CloseOutlined } from "@mui/icons-material";
99

@@ -52,8 +52,7 @@ const ConfigEditor = ({ catalogItem }: { catalogItem: CatalogItemWithName }) =>
5252
if (!configSchema) return;
5353

5454
try {
55-
const schema = new JsonSchema.Draft2019(configSchema[0]);
56-
const template = schema.getTemplate(existingConfigForItem);
55+
const template = getTemplateForItem(catalogItem, existingConfigForItem);
5756
setConfig(template);
5857
setLocalConfig(deepFlattenObject(template));
5958
} catch (error) {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type TileProps = {
2525

2626
const Tile = ({ item, registered, onSecretChange, secrets, client, unAssignedConfig }: TileProps) => {
2727
const loadAssignedSecrets = () => {
28-
const assignedSecrets = Secrets.getAssignedSecrets(item, secrets);
28+
const assignedSecrets = Secrets.getSecretsWithAssignment(item, secrets);
2929
setAssignedSecrets(assignedSecrets)
3030
}
3131

@@ -104,7 +104,6 @@ const Tile = ({ item, registered, onSecretChange, secrets, client, unAssignedCon
104104
}
105105
}}
106106
onSecretChange={onSecretChange}
107-
unAssignedSecrets={unAssignedSecrets}
108107
/>
109108
<Card onClick={(e) => {
110109
if ((e.target as HTMLElement).tagName !== 'INPUT') {

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

Lines changed: 50 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { Alert, Badge, Box, ButtonGroup, Chip, CircularProgress, Dialog, DialogContent, DialogTitle, Divider, FormControlLabel, FormHelperText, Grid2, IconButton, Link, Modal, Paper, Stack, Switch, Tab, Tabs, TextField, Tooltip, Typography, useTheme } from "@mui/material";
1+
import { Alert, Badge, Box, ButtonGroup, Chip, CircularProgress, Dialog, DialogContent, DialogTitle, Divider, FormControlLabel, FormHelperText, Grid2, IconButton, Link, ListItem, Modal, Paper, Stack, Switch, Tab, Tabs, TextField, Tooltip, Typography, useTheme } from "@mui/material";
22
import { CheckOutlined, Close, CloseOutlined, Code, Delete, DeleteOutline, DeleteOutlined, LockReset, Save, SaveOutlined } from "@mui/icons-material";
3-
import { useEffect, useState } from "react";
3+
import { useEffect, useMemo, useState } from "react";
44
import { CatalogItemWithName } from "../../types/catalog";
55
import Secrets from "../../Secrets";
66
import { v1 } from "@docker/extension-api-client-types";
77
import { useCatalogContext } from "../../context/CatalogContext";
88
import { useConfigContext } from "../../context/ConfigContext";
9-
import { deepFlattenObject, deepGet, deepSet, mergeDeep } from "../../MergeDeep";
10-
import { DeepObject } from "../../types/utils";
11-
import { Parameter, ParameterArray, ParameterObject, Parameters, ParsedParameters, Config } from "../../types/config";
12-
import { Ref } from "../../Refs";
139
import { ASSIGNED_SECRET_PLACEHOLDER, CATALOG_LAYOUT_SX, UNASSIGNED_SECRET_PLACEHOLDER } from "../../Constants";
14-
import JsonSchemaLibrary from "json-schema-library";
1510
import ConfigEditor from "./ConfigEditor";
1611

1712
// Styles for the tab panel
@@ -42,11 +37,6 @@ function TabPanel(props: TabPanelProps) {
4237
);
4338
}
4439

45-
// Define types reference
46-
const types = ['string', 'number', 'boolean', 'array', 'object'] as const;
47-
48-
49-
5040
interface ConfigurationModalProps {
5141
open: boolean;
5242
onClose: () => void;
@@ -55,7 +45,6 @@ interface ConfigurationModalProps {
5545
onToggleRegister: (checked: boolean) => void;
5646
registered: boolean;
5747
onSecretChange: (secret: { name: string, value: string }) => Promise<void>;
58-
unAssignedSecrets: { name: string, assigned: boolean }[];
5948
}
6049

6150

@@ -67,42 +56,13 @@ const ConfigurationModal = ({
6756
onToggleRegister,
6857
registered,
6958
onSecretChange,
70-
unAssignedSecrets,
7159
}: ConfigurationModalProps) => {
7260

73-
const { registryItems, secrets } = useCatalogContext();
74-
const { config, configLoading } = useConfigContext();
61+
const { registryItems, secrets, getCanRegisterCatalogItem } = useCatalogContext();
62+
const { configLoading, config } = useConfigContext();
7563
const [localSecrets, setLocalSecrets] = useState<{ [key: string]: string | undefined }>({});
7664
const theme = useTheme();
7765

78-
// Helper function to get default values based on type
79-
const getDefaultValue = (schema: any) => {
80-
if (!schema || !schema.type) return '';
81-
82-
switch (schema.type) {
83-
case 'string':
84-
return schema.default || '';
85-
case 'number':
86-
case 'integer':
87-
return schema.default || 0;
88-
case 'boolean':
89-
return schema.default || false;
90-
case 'array':
91-
return schema.default || [];
92-
case 'object':
93-
if (schema.properties) {
94-
const objTemplate: Record<string, any> = {};
95-
Object.entries(schema.properties).forEach(([key, propSchema]: [string, any]) => {
96-
objTemplate[key] = getDefaultValue(propSchema);
97-
});
98-
return schema.default || objTemplate;
99-
}
100-
return schema.default || {};
101-
default:
102-
return '';
103-
}
104-
};
105-
10666
const toolChipStyle = {
10767
padding: '2px 8px',
10868
justifyContent: 'center',
@@ -128,7 +88,7 @@ const ConfigurationModal = ({
12888

12989
// Load assigned secrets
13090
useEffect(() => {
131-
const loadedSecrets = Secrets.getAssignedSecrets(catalogItem, secrets);
91+
const loadedSecrets = Secrets.getSecretsWithAssignment(catalogItem, secrets);
13292
setAssignedSecrets(loadedSecrets);
13393
setLocalSecrets(loadedSecrets.reduce((acc, secret) => {
13494
acc[secret.name] = secret.assigned ? ASSIGNED_SECRET_PLACEHOLDER : '';
@@ -142,25 +102,16 @@ const ConfigurationModal = ({
142102
setTabValue(newValue);
143103
};
144104

145-
// Determine if we should show the secrets tab and config tab
146-
const hasSecrets = assignedSecrets.length > 0;
147-
const hasConfig = catalogItem.config && catalogItem.config.length > 0;
105+
// Memoize the canRegister value to prevent unnecessary recalculations
106+
const canRegister = useMemo(() => getCanRegisterCatalogItem(catalogItem), [getCanRegisterCatalogItem, catalogItem, config]);
148107

149-
// If there's only one tab to show, automatically select it
150-
useEffect(() => {
151-
if (!hasSecrets && hasConfig || hasSecrets && !hasConfig) {
152-
setTabValue(0); // Config tab
153-
}
154-
}, [hasSecrets, hasConfig]);
155-
156-
const hasAllSecrets = unAssignedSecrets.length === 0
157-
const emptyConfig = catalogItem.config && (!config?.[catalogItem.name] || Object.keys(config?.[catalogItem.name] || {}).length === 0)
108+
const contributesNoConfigOrSecrets = (!catalogItem.config || catalogItem.config.length === 0) && (!catalogItem.secrets || catalogItem.secrets.length === 0);
158109

159110
useEffect(() => {
160-
if (!hasAllSecrets || emptyConfig) {
161-
setTabValue(1);
111+
if (!canRegister && !contributesNoConfigOrSecrets) {
112+
setTabValue(1); // Config tab
162113
}
163-
}, [hasAllSecrets, emptyConfig]);
114+
}, [canRegister, contributesNoConfigOrSecrets]);
164115

165116

166117
if (!registryItems) {
@@ -200,8 +151,8 @@ const ConfigurationModal = ({
200151
<Typography sx={{ mt: 2, maxHeight: '5em', overflow: 'auto' }} color="text.secondary">
201152
{catalogItem.description}
202153
</Typography>
203-
<Tooltip placement="right" title={!hasAllSecrets || emptyConfig ? 'You must assign all secrets and configure the item before it can be used.' : ''}>
204-
<FormControlLabel control={<Switch disabled={!hasAllSecrets || emptyConfig} checked={registered} onChange={(e) => onToggleRegister(e.target.checked)} />} label={registered ? 'Disable ' + `${catalogItem.name} tools` : 'Enable ' + `${catalogItem.name} tools`} sx={{ mt: 2 }} />
154+
<Tooltip placement="right" title={!canRegister ? 'You must assign all secrets and configure the item before it can be used.' : ''}>
155+
<FormControlLabel control={<Switch disabled={!canRegister} checked={registered} onChange={(e) => onToggleRegister(e.target.checked)} />} label={registered ? 'Disable ' + `${catalogItem.name} tools` : 'Enable ' + `${catalogItem.name} tools`} sx={{ mt: 2 }} />
205156
</Tooltip>
206157
<Divider sx={{ mt: 2 }} />
207158
<Typography variant="caption" sx={{ mt: 2, color: 'text.secondary' }}>
@@ -220,10 +171,9 @@ const ConfigurationModal = ({
220171
<Tabs value={tabValue} onChange={handleTabChange}>
221172
<Tab label="Tools" />
222173
{/* <Tab label="Prompts" /> */}
223-
<Tab disabled={!hasSecrets && !hasConfig} label={<Badge invisible={hasAllSecrets && !emptyConfig} sx={{ pl: 1, pr: 1 }} variant="dot" badgeContent={hasSecrets ? 'Secrets' : 'Config'} color="error">Config & Secrets</Badge>} />
174+
<Tab disabled={contributesNoConfigOrSecrets} label={<Badge invisible={canRegister} sx={{ pl: 1, pr: 1 }} variant="dot" badgeContent={catalogItem.config && catalogItem.config.length > 0 ? 'Secrets' : 'Config'} color="error">Config & Secrets</Badge>} />
224175
</Tabs>
225176
</Box>
226-
227177
<TabPanel value={tabValue} index={0} >
228178
{!catalogItem?.tools?.length && (
229179
<Typography>
@@ -247,46 +197,51 @@ const ConfigurationModal = ({
247197
<Stack direction="column" spacing={2} sx={{ border: '2px solid', borderColor: theme.palette.warning.contrastText, borderRadius: 2, p: 2, mt: 2 }}>
248198
<ConfigEditor catalogItem={catalogItem} />
249199
<Typography variant="h6" sx={{ mb: 1 }}>Secrets</Typography>
250-
{assignedSecrets?.map(secret => {
251-
const secretEdited = secret.assigned ? localSecrets[secret.name] !== ASSIGNED_SECRET_PLACEHOLDER : localSecrets[secret.name] !== '';
252-
return (
253-
<Stack key={secret.name} direction="row" spacing={2} alignItems="center">
254-
<TextField key={secret.name} label={secret.name} value={localSecrets[secret.name]} fullWidth onChange={(e) => {
255-
setLocalSecrets({ ...localSecrets, [secret.name]: e.target.value });
256-
}} type='password' />
257-
{secret.assigned && !secretEdited && <IconButton size="small" color="error" onClick={() => {
258-
setLocalSecrets({ ...localSecrets, [secret.name]: UNASSIGNED_SECRET_PLACEHOLDER });
259-
onSecretChange({ name: secret.name, value: UNASSIGNED_SECRET_PLACEHOLDER });
260-
}}>
261-
<DeleteOutlined />
262-
</IconButton>}
263-
{secretEdited && <ButtonGroup>
264-
<IconButton onClick={async () => {
265-
await onSecretChange({ name: secret.name, value: localSecrets[secret.name]! });
266-
}}>
267-
<CheckOutlined sx={{ color: 'success.main' }} />
268-
</IconButton>
269-
<IconButton onClick={async () => {
270-
setLocalSecrets({ ...localSecrets, [secret.name]: secret.assigned ? ASSIGNED_SECRET_PLACEHOLDER : '' });
271-
}}>
272-
<CloseOutlined sx={{ color: 'error.main' }} />
273-
</IconButton>
274-
</ButtonGroup>}
275-
</Stack>
200+
{
201+
catalogItem.secrets && catalogItem.secrets?.length > 0 ? (
202+
assignedSecrets?.map(secret => {
203+
const secretEdited = secret.assigned ? localSecrets[secret.name] !== ASSIGNED_SECRET_PLACEHOLDER : localSecrets[secret.name] !== '';
204+
return (
205+
<Stack key={secret.name} direction="row" spacing={2} alignItems="center">
206+
<TextField key={secret.name} label={secret.name} value={localSecrets[secret.name]} fullWidth onChange={(e) => {
207+
setLocalSecrets({ ...localSecrets, [secret.name]: e.target.value });
208+
}} type='password' />
209+
{secret.assigned && !secretEdited && <IconButton size="small" color="error" onClick={() => {
210+
setLocalSecrets({ ...localSecrets, [secret.name]: UNASSIGNED_SECRET_PLACEHOLDER });
211+
onSecretChange({ name: secret.name, value: UNASSIGNED_SECRET_PLACEHOLDER });
212+
}}>
213+
<DeleteOutlined />
214+
</IconButton>}
215+
{secretEdited && <ButtonGroup>
216+
<IconButton onClick={async () => {
217+
await onSecretChange({ name: secret.name, value: localSecrets[secret.name]! });
218+
}}>
219+
<CheckOutlined sx={{ color: 'success.main' }} />
220+
</IconButton>
221+
<IconButton onClick={async () => {
222+
setLocalSecrets({ ...localSecrets, [secret.name]: secret.assigned ? ASSIGNED_SECRET_PLACEHOLDER : '' });
223+
}}>
224+
<CloseOutlined sx={{ color: 'error.main' }} />
225+
</IconButton>
226+
</ButtonGroup>}
227+
</Stack>
228+
)
229+
})) : (
230+
<Alert severity="info">No secrets available for this item.</Alert>
276231
)
277-
})}
232+
}
278233
</Stack>
279234
</Stack>
280-
</TabPanel>
235+
</TabPanel >
281236
<TabPanel value={tabValue} index={2}>
282237
<Typography>Examples</Typography>
283238
WIP
284239
</TabPanel>
285240
</>
286241
)}
287-
</Paper>
288-
</Modal>
289-
);
242+
</Paper >
243+
</Modal >
244+
)
290245
};
291246

292247
export default ConfigurationModal;

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Badge, CardMedia, IconButton, Stack, Switch, Tooltip, Typography } from
33
import { getUnsupportedSecretMessage } from "../../Constants";
44
import { CatalogItemWithName } from "../../types/catalog";
55
import { useConfigContext } from "../../context/ConfigContext";
6+
import { useCatalogContext } from "../../context/CatalogContext";
67

78
type TopProps = {
89
unAssignedConfig: { name: string, assigned: boolean }[],
@@ -12,16 +13,13 @@ type TopProps = {
1213
item: CatalogItemWithName
1314
}
1415

15-
export default function Top({ item, unAssignedConfig, onToggleRegister, unAssignedSecrets, registered }: TopProps) {
16+
export default function Top({ item, onToggleRegister, registered }: TopProps) {
17+
const { getCanRegisterCatalogItem } = useCatalogContext();
1618

17-
const hasAllSecrets = unAssignedSecrets.length === 0
18-
19-
const { config } = useConfigContext()
20-
21-
const missingConfig = !config?.[item.name] || Object.keys(config?.[item.name] || {}).length === 0
19+
const canRegister = getCanRegisterCatalogItem(item);
2220

2321
const getActionButton = () => {
24-
if (!hasAllSecrets || missingConfig) {
22+
if (!canRegister) {
2523
return <Stack direction="row" spacing={0} alignItems="center">
2624
<Tooltip title="This tile needs configuration before it can be used.">
2725
<span>

0 commit comments

Comments
 (0)