Skip to content

Commit 3fad398

Browse files
authored
Merge pull request #177 from docker/miscellaneous-fixes
Miscellaneous UI fixes
2 parents adab600 + 9fca9e3 commit 3fad398

File tree

3 files changed

+254
-324
lines changed

3 files changed

+254
-324
lines changed
Lines changed: 136 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,143 @@
1-
import { v1 } from "@docker/extension-api-client-types";
2-
import CheckOutlined from "@mui/icons-material/CheckOutlined";
3-
import CloseOutlined from "@mui/icons-material/CloseOutlined";
4-
import { Alert, CircularProgress, IconButton, Stack, TextField, Typography } from "@mui/material";
5-
import * as JsonSchema from "json-schema-library";
6-
import { useEffect, useMemo, useState } from "react";
7-
8-
import { buildObjectFromFlattenedObject, deepFlattenObject } from "../../MergeDeep";
9-
import { useConfig } from "../../queries/useConfig";
10-
import { CatalogItemRichened } from "../../types/catalog";
1+
import { v1 } from '@docker/extension-api-client-types';
2+
import CheckOutlined from '@mui/icons-material/CheckOutlined';
3+
import CloseOutlined from '@mui/icons-material/CloseOutlined';
4+
import {
5+
Alert,
6+
CircularProgress,
7+
IconButton,
8+
OutlinedInput,
9+
Stack,
10+
TextField,
11+
Typography,
12+
} from '@mui/material';
13+
import * as JsonSchema from 'json-schema-library';
14+
import { useEffect, useMemo, useState } from 'react';
15+
16+
import {
17+
buildObjectFromFlattenedObject,
18+
deepFlattenObject,
19+
} from '../../MergeDeep';
20+
import { useConfig } from '../../queries/useConfig';
21+
import { CatalogItemRichened } from '../../types/catalog';
1122

1223
JsonSchema.settings.GET_TEMPLATE_RECURSION_LIMIT = 1000;
1324
JsonSchema.settings.templateDefaultOptions.addOptionalProps = true;
1425

15-
const EmptyState = () => {
16-
return (
17-
<Alert severity="info"> No config available </Alert>
18-
)
19-
}
20-
21-
const LoadingState = () => {
22-
return (
23-
<Stack direction="row" spacing={2} alignItems="center">
24-
<CircularProgress />
25-
<Typography>Loading config...</Typography>
26-
</Stack>
27-
)
28-
}
29-
30-
const ConfigEditor = ({ catalogItem, client }: { catalogItem: CatalogItemRichened, client: v1.DockerDesktopClient }) => {
31-
const configSchema = catalogItem.configSchema;
32-
33-
const { config, saveConfig: updateExistingConfig, configLoading } = useConfig(client);
34-
35-
const existingConfigForItem = catalogItem.configValue || {};
36-
37-
38-
const [localConfig, setLocalConfig] = useState<{ [key: string]: any } | undefined>(undefined);
39-
const [savingKeys, setSavingKeys] = useState<Set<string>>(new Set());
40-
41-
// Use memoized flattenedConfig to ensure it only updates when config changes
42-
// This MUST be called before any early returns to avoid conditional hook calls
43-
const flattenedConfig = useMemo(() =>
44-
configSchema ? deepFlattenObject({ ...catalogItem.configTemplate, ...existingConfigForItem }) : {},
45-
[catalogItem.configTemplate, existingConfigForItem, configSchema]);
46-
47-
// Reset local config when the existing config changes
48-
useEffect(() => {
49-
if (!configSchema) return;
50-
setLocalConfig(flattenedConfig);
51-
}, [flattenedConfig]);
52-
53-
// Early returns
54-
if (!configSchema) {
55-
return <EmptyState />;
56-
}
57-
58-
if (!config && !configLoading) {
59-
return <EmptyState />;
60-
}
61-
62-
if (configLoading) {
63-
return <LoadingState />;
64-
}
65-
66-
if (!config || !localConfig) {
67-
return <LoadingState />;
68-
}
69-
70-
return (
71-
<Stack>
72-
<Typography variant="h6">Config</Typography>
73-
<Stack direction="column" spacing={2}>
74-
{Object.keys(flattenedConfig).map((key: string) => {
75-
const edited = localConfig[key] !== flattenedConfig[key];
76-
const isSaving = savingKeys.has(key);
77-
78-
return (
79-
<Stack key={key} direction="row" spacing={2}>
80-
<TextField
81-
label={key}
82-
value={localConfig[key] || ''}
83-
onChange={(e) => setLocalConfig({ ...localConfig, [key]: e.target.value })}
84-
disabled={isSaving}
85-
/>
86-
{edited && <Stack direction="row" spacing={2}>
87-
{isSaving ? (
88-
<CircularProgress size={24} />
89-
) : (
90-
<Stack direction="row" spacing={2}>
91-
<IconButton onClick={() => updateExistingConfig(catalogItem.name, buildObjectFromFlattenedObject(localConfig))}
92-
disabled={isSaving}
93-
>
94-
<CheckOutlined sx={{ color: 'success.main' }} />
95-
</IconButton>
96-
<IconButton onClick={() => setLocalConfig({
97-
...localConfig,
98-
[key]: flattenedConfig[key]
99-
})}
100-
disabled={isSaving}
101-
>
102-
<CloseOutlined sx={{ color: 'error.main' }} />
103-
</IconButton>
104-
</Stack>
105-
)}
106-
</Stack>}
107-
</Stack>
108-
)
109-
})}
26+
const ConfigEditor = ({
27+
catalogItem,
28+
client,
29+
}: {
30+
catalogItem: CatalogItemRichened;
31+
client: v1.DockerDesktopClient;
32+
}) => {
33+
const configSchema = catalogItem.configSchema;
34+
35+
const {
36+
config,
37+
saveConfig: updateExistingConfig,
38+
configLoading,
39+
} = useConfig(client);
40+
41+
const existingConfigForItem = catalogItem.configValue || {};
42+
43+
const [localConfig, setLocalConfig] = useState<
44+
{ [key: string]: any } | undefined
45+
>(undefined);
46+
const [savingKeys, setSavingKeys] = useState<Set<string>>(new Set());
47+
48+
// Use memoized flattenedConfig to ensure it only updates when config changes
49+
// This MUST be called before any early returns to avoid conditional hook calls
50+
const flattenedConfig = useMemo(
51+
() =>
52+
configSchema
53+
? deepFlattenObject({
54+
...catalogItem.configTemplate,
55+
...existingConfigForItem,
56+
})
57+
: {},
58+
[catalogItem.configTemplate, existingConfigForItem, configSchema]
59+
);
60+
61+
// Reset local config when the existing config changes
62+
useEffect(() => {
63+
if (!configSchema) return;
64+
setLocalConfig(flattenedConfig);
65+
}, [flattenedConfig]);
66+
67+
// Early returns
68+
if (!configSchema) {
69+
return null;
70+
}
71+
72+
if (!config && !configLoading) {
73+
return null;
74+
}
75+
76+
if (configLoading) {
77+
return null;
78+
}
79+
80+
if (!config || !localConfig) {
81+
return null;
82+
}
83+
84+
return (
85+
<Stack>
86+
<Typography variant="subtitle2">Config</Typography>
87+
<Stack direction="column" spacing={2}>
88+
{Object.keys(flattenedConfig).map((key: string) => {
89+
const edited = localConfig[key] !== flattenedConfig[key];
90+
const isSaving = savingKeys.has(key);
91+
92+
return (
93+
<Stack key={key} direction="row" spacing={2}>
94+
<OutlinedInput
95+
size="small"
96+
placeholder={key}
97+
value={localConfig[key] || ''}
98+
onChange={(e) =>
99+
setLocalConfig({ ...localConfig, [key]: e.target.value })
100+
}
101+
disabled={isSaving}
102+
/>
103+
{edited && (
104+
<Stack direction="row" spacing={2}>
105+
{isSaving ? (
106+
<CircularProgress size={24} />
107+
) : (
108+
<Stack direction="row" spacing={2}>
109+
<IconButton
110+
onClick={() =>
111+
updateExistingConfig(
112+
catalogItem.name,
113+
buildObjectFromFlattenedObject(localConfig)
114+
)
115+
}
116+
disabled={isSaving}
117+
>
118+
<CheckOutlined sx={{ color: 'success.main' }} />
119+
</IconButton>
120+
<IconButton
121+
onClick={() =>
122+
setLocalConfig({
123+
...localConfig,
124+
[key]: flattenedConfig[key],
125+
})
126+
}
127+
disabled={isSaving}
128+
>
129+
<CloseOutlined sx={{ color: 'error.main' }} />
130+
</IconButton>
131+
</Stack>
132+
)}
133+
</Stack>
134+
)}
110135
</Stack>
111-
</Stack>
112-
)
113-
}
136+
);
137+
})}
138+
</Stack>
139+
</Stack>
140+
);
141+
};
114142

115143
export default ConfigEditor;

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

Lines changed: 11 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
import { v1 } from '@docker/extension-api-client-types';
2-
import LockReset from '@mui/icons-material/LockReset';
3-
import Save from '@mui/icons-material/Save';
42
import {
53
Card,
64
CardActionArea,
75
CardContent,
86
CircularProgress,
9-
Dialog,
10-
DialogContent,
11-
DialogTitle,
12-
IconButton,
137
Stack,
14-
TextField,
15-
Typography
8+
Typography,
169
} from '@mui/material';
1710
import { useState } from 'react';
1811

19-
import { MCP_POLICY_NAME } from '../../Constants';
2012
import { useCatalogOperations } from '../../queries/useCatalog';
2113
import { useSecrets } from '../../queries/useSecrets';
2214
import { CatalogItemRichened } from '../../types/catalog';
@@ -32,15 +24,9 @@ type TileProps = {
3224
};
3325

3426
const Tile = ({ item, client, registryLoading }: TileProps) => {
35-
const [showSecretDialog, setShowSecretDialog] = useState(false);
3627
const [assignedSecrets] = useState<{ name: string; assigned: boolean }[]>([]);
37-
const [changedSecrets, setChangedSecrets] = useState<{
38-
[key: string]: string | undefined;
39-
}>({});
40-
const [secretLoading, setSecretLoading] = useState(false);
4128
const [showConfigModal, setShowConfigModal] = useState(false);
42-
const { isLoading: secretsLoading, mutate: mutateSecret } =
43-
useSecrets(client);
29+
const { isLoading: secretsLoading } = useSecrets(client);
4430
const { registerCatalogItem, unregisterCatalogItem } =
4531
useCatalogOperations(client);
4632

@@ -57,94 +43,15 @@ const Tile = ({ item, client, registryLoading }: TileProps) => {
5743

5844
return (
5945
<>
60-
<Dialog
61-
open={showSecretDialog}
62-
onClose={() => setShowSecretDialog(false)}
63-
>
64-
<DialogTitle>
65-
<Typography variant="h6">Secrets</Typography>
66-
</DialogTitle>
67-
<DialogContent>
68-
<Stack direction="column" spacing={2}>
69-
{assignedSecrets?.map((secret) => {
70-
const isAssigned = assignedSecrets.find(
71-
(s) => s.name === secret.name
72-
);
73-
return (
74-
<Stack
75-
key={secret.name}
76-
direction="row"
77-
spacing={2}
78-
alignItems="center"
79-
>
80-
<Typography variant="body2">
81-
{secret.name}{' '}
82-
{isAssigned?.assigned ? 'assigned' : 'not assigned'}
83-
</Typography>
84-
<TextField
85-
placeholder={
86-
isAssigned?.assigned ? '********' : 'Enter secret value'
87-
}
88-
type="password"
89-
key={secret.name}
90-
label={secret.name}
91-
value={changedSecrets[secret.name] || ''}
92-
onChange={(event) =>
93-
setChangedSecrets({
94-
...changedSecrets,
95-
[secret.name]: event.target.value,
96-
})
97-
}
98-
/>
99-
{isAssigned?.assigned && changedSecrets[secret.name] && (
100-
<IconButton
101-
onClick={() =>
102-
setChangedSecrets({
103-
...changedSecrets,
104-
[secret.name]: undefined,
105-
})
106-
}
107-
>
108-
<LockReset />
109-
</IconButton>
110-
)}
111-
{changedSecrets[secret.name] && (
112-
<IconButton
113-
onClick={() => {
114-
setSecretLoading(true);
115-
mutateSecret
116-
.mutateAsync({
117-
name: secret.name,
118-
value: changedSecrets[secret.name] || '',
119-
policies: [MCP_POLICY_NAME],
120-
})
121-
.then(() => {
122-
setSecretLoading(false);
123-
const newChangedSecrets = { ...changedSecrets };
124-
delete newChangedSecrets[secret.name];
125-
setChangedSecrets(newChangedSecrets);
126-
});
127-
}}
128-
>
129-
{secretLoading ? (
130-
<CircularProgress size={20} />
131-
) : (
132-
<Save />
133-
)}
134-
</IconButton>
135-
)}
136-
</Stack>
137-
);
138-
})}
139-
</Stack>
140-
</DialogContent>
141-
</Dialog>
142-
{showConfigModal && <ConfigurationModal
143-
open={showConfigModal}
144-
onClose={() => setShowConfigModal(false)}
145-
catalogItem={item}
146-
client={client}
147-
/>}
46+
{showConfigModal && (
47+
<ConfigurationModal
48+
open={showConfigModal}
49+
onClose={() => setShowConfigModal(false)}
50+
catalogItem={item}
51+
client={client}
52+
registryLoading={registryLoading}
53+
/>
54+
)}
14855
<Card>
14956
<CardActionArea
15057
sx={{ padding: 1.5 }}

0 commit comments

Comments
 (0)