Skip to content

Commit 2767033

Browse files
authored
Merge pull request #148 from docker/trungutt/rework-tile-description
Rework Tile Description
2 parents 3639d83 + 93e3827 commit 2767033

File tree

3 files changed

+199
-116
lines changed

3 files changed

+199
-116
lines changed

src/extension/ui/src/Constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ export const DOCKER_MCP_IMAGE = 'alpine/socat'
1010
export const DOCKER_MCP_CONTAINER_ARGS = 'STDIO TCP:host.docker.internal:8811'
1111
export const DOCKER_MCP_COMMAND = `docker run -i --rm ${DOCKER_MCP_IMAGE} ${DOCKER_MCP_CONTAINER_ARGS}`
1212

13-
export const TILE_DESCRIPTION_MAX_LENGTH = 120;
14-
1513
export const CATALOG_LAYOUT_SX = {
1614
width: '90vw',
1715
maxWidth: '1200px',
Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
import { Tooltip, Typography } from "@mui/material";
2-
import { CatalogItemRichened } from "../../types"
3-
import { TILE_DESCRIPTION_MAX_LENGTH } from "../../Constants";
1+
import { Typography } from '@mui/material';
42

5-
type CenterProps = {
6-
item: CatalogItemRichened;
7-
}
3+
import type { CatalogItemRichened } from '../../types';
84

9-
export default function Center({ item }: CenterProps) {
10-
return (
11-
<Tooltip title={item.description} sx={{ height: '100%' }}>
12-
<Typography variant="body2" sx={{ color: 'text.secondary', height: 50 }}>
13-
{item.description?.slice(0, TILE_DESCRIPTION_MAX_LENGTH)}
14-
{item.description?.length && item.description.length > TILE_DESCRIPTION_MAX_LENGTH && '...'}
15-
</Typography>
16-
</Tooltip>
17-
)
18-
}
5+
/*
6+
`Center` displays the description of the item in a truncated format.
7+
This should have been renamed to `Description`.
8+
*/
9+
export default function Center({ item }: { item: CatalogItemRichened }) {
10+
return (
11+
<Typography
12+
variant="body2"
13+
sx={{
14+
color: 'text.secondary',
15+
// These CSS properties are used to create a multiline ellipsis effect: 3 lines maximum for the description
16+
display: '-webkit-box',
17+
textOverflow: 'ellipsis',
18+
overflow: 'hidden',
19+
WebkitBoxOrient: 'vertical',
20+
WebkitLineClamp: '3',
21+
height: 48,
22+
}}
23+
>
24+
{item.description ?? ''}
25+
</Typography>
26+
);
27+
}
Lines changed: 174 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,182 @@
1-
import { CircularProgress, Dialog, DialogContent, DialogTitle, Divider, IconButton, Stack, TextField, Typography } from "@mui/material";
2-
import { Card, CardContent } from "@mui/material";
3-
import { useEffect, useState } from "react";
4-
import { CatalogItemRichened } from "../../types/catalog";
5-
import { Save, LockReset } from "@mui/icons-material";
6-
import ConfigurationModal from "./Modal";
7-
import Top from "./Top";
8-
import Center from "./Center";
9-
import Bottom from "./Bottom";
10-
import { v1 } from "@docker/extension-api-client-types";
11-
import { useSecrets } from "../../queries/useSecrets";
12-
import { useCatalogAll, useCatalogOperations } from "../../queries/useCatalog";
13-
import { MCP_POLICY_NAME } from "../../Constants";
1+
import { v1 } from '@docker/extension-api-client-types';
2+
import { LockReset, Save } from '@mui/icons-material';
3+
import {
4+
Card,
5+
CardActionArea,
6+
CardContent,
7+
CircularProgress,
8+
Dialog,
9+
DialogContent,
10+
DialogTitle,
11+
Divider,
12+
IconButton,
13+
Stack,
14+
TextField,
15+
Typography,
16+
} from '@mui/material';
17+
import { useState } from 'react';
18+
19+
import { MCP_POLICY_NAME } from '../../Constants';
20+
import { useCatalogAll, useCatalogOperations } from '../../queries/useCatalog';
21+
import { useSecrets } from '../../queries/useSecrets';
22+
import { CatalogItemRichened } from '../../types/catalog';
23+
import Bottom from './Bottom';
24+
import Center from './Center';
25+
import ConfigurationModal from './Modal';
26+
import Top from './Top';
1427

1528
type TileProps = {
16-
item: CatalogItemRichened;
17-
client: v1.DockerDesktopClient;
18-
}
29+
item: CatalogItemRichened;
30+
client: v1.DockerDesktopClient;
31+
};
1932

2033
const Tile = ({ item, client }: TileProps) => {
34+
const [showSecretDialog, setShowSecretDialog] = useState(false);
35+
const [assignedSecrets] = useState<{ name: string; assigned: boolean }[]>([]);
36+
const [changedSecrets, setChangedSecrets] = useState<{
37+
[key: string]: string | undefined;
38+
}>({});
39+
const [secretLoading, setSecretLoading] = useState(false);
40+
const [showConfigModal, setShowConfigModal] = useState(false);
41+
const { isLoading: secretsLoading, mutate: mutateSecret } =
42+
useSecrets(client);
43+
const { registerCatalogItem, unregisterCatalogItem } =
44+
useCatalogOperations(client);
45+
const { registryLoading } = useCatalogAll(client);
2146

22-
const [showSecretDialog, setShowSecretDialog] = useState(false)
23-
const [assignedSecrets] = useState<{ name: string, assigned: boolean }[]>([])
24-
const [changedSecrets, setChangedSecrets] = useState<{ [key: string]: string | undefined }>({})
25-
const [secretLoading, setSecretLoading] = useState(false)
26-
const [showConfigModal, setShowConfigModal] = useState(false)
27-
const { isLoading: secretsLoading, mutate: mutateSecret } = useSecrets(client)
28-
const { registerCatalogItem, unregisterCatalogItem } = useCatalogOperations(client)
29-
const { registryLoading } = useCatalogAll(client)
30-
31-
if (registryLoading || secretsLoading) {
32-
return <>
33-
<CircularProgress size={20} />
34-
<Typography>Loading registry...</Typography>
35-
</>
36-
}
47+
if (registryLoading || secretsLoading) {
48+
return (
49+
<>
50+
<CircularProgress size={20} />
51+
<Typography>Loading registry...</Typography>
52+
</>
53+
);
54+
}
3755

38-
const unAssignedSecrets = assignedSecrets.filter(s => !s.assigned)
56+
const unAssignedSecrets = assignedSecrets.filter((s) => !s.assigned);
3957

40-
return (
41-
<>
42-
<Dialog open={showSecretDialog} onClose={() => setShowSecretDialog(false)}>
43-
<DialogTitle>
44-
<Typography variant="h6">
45-
Secrets
46-
</Typography>
47-
</DialogTitle>
48-
<DialogContent>
49-
<Stack direction="column" spacing={2}>
50-
{assignedSecrets?.map(secret => {
51-
const isAssigned = assignedSecrets.find(s => s.name === secret.name)
52-
return (
53-
<Stack key={secret.name} direction="row" spacing={2} alignItems="center">
54-
<Typography variant="body2">{secret.name} {isAssigned?.assigned ? 'assigned' : 'not assigned'}</Typography>
55-
<TextField placeholder={isAssigned?.assigned ? '********' : 'Enter secret value'} type="password" key={secret.name} label={secret.name} value={changedSecrets[secret.name] || ''} onChange={(event) => setChangedSecrets({ ...changedSecrets, [secret.name]: event.target.value })} />
56-
{isAssigned?.assigned && changedSecrets[secret.name] && <IconButton onClick={() => setChangedSecrets({ ...changedSecrets, [secret.name]: undefined })}>
57-
<LockReset />
58-
</IconButton>}
59-
{changedSecrets[secret.name] && <IconButton onClick={() => {
60-
setSecretLoading(true)
61-
mutateSecret.mutateAsync({ name: secret.name, value: changedSecrets[secret.name] || '', policies: [MCP_POLICY_NAME] }).then(() => {
62-
setSecretLoading(false)
63-
const newChangedSecrets = { ...changedSecrets }
64-
delete newChangedSecrets[secret.name]
65-
setChangedSecrets(newChangedSecrets)
66-
})
67-
}}>
68-
{secretLoading ? <CircularProgress size={20} /> : <Save />}
69-
</IconButton>}
70-
</Stack>
71-
)
72-
})}
73-
</Stack>
74-
</DialogContent>
75-
</Dialog>
76-
<ConfigurationModal
77-
open={showConfigModal}
78-
onClose={() => setShowConfigModal(false)}
79-
catalogItem={item}
80-
client={client}
81-
/>
82-
<Card onClick={(e) => {
83-
if ((e.target as HTMLElement).tagName !== 'INPUT') {
84-
setShowConfigModal(true)
85-
}
86-
}} sx={{ height: 150, borderColor: 'divider', borderWidth: 1, borderStyle: 'solid', p: 0, cursor: 'pointer', transition: 'background-color 0.1s ease', '&:hover': { backgroundColor: 'action.hover' } }} >
87-
<CardContent sx={{ paddingBottom: 0, paddingTop: 2 }}>
88-
<Stack direction="column" spacing={0}>
89-
<Top onToggleRegister={(checked) => {
90-
if (checked) {
91-
registerCatalogItem(item)
92-
} else {
93-
unregisterCatalogItem(item)
94-
}
95-
}} item={item} />
96-
<Center item={item} />
97-
<Divider sx={{ marginBottom: 1 }} />
98-
<Bottom item={item} needsConfiguration={Boolean(unAssignedSecrets.length)} />
99-
</Stack>
100-
</CardContent>
101-
</Card >
102-
</>
103-
)
104-
}
58+
return (
59+
<>
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+
<ConfigurationModal
143+
open={showConfigModal}
144+
onClose={() => setShowConfigModal(false)}
145+
catalogItem={item}
146+
client={client}
147+
/>
148+
<Card sx={{ height: 150 }}>
149+
<CardActionArea
150+
onClick={(e) => {
151+
if ((e.target as HTMLElement).tagName !== 'INPUT') {
152+
setShowConfigModal(true);
153+
}
154+
}}
155+
>
156+
<CardContent sx={{ paddingBottom: 0, paddingTop: 2 }}>
157+
<Stack direction="column" spacing={0}>
158+
<Top
159+
onToggleRegister={(checked) => {
160+
if (checked) {
161+
registerCatalogItem(item);
162+
} else {
163+
unregisterCatalogItem(item);
164+
}
165+
}}
166+
item={item}
167+
/>
168+
<Center item={item} />
169+
<Divider sx={{ marginBottom: 1 }} />
170+
<Bottom
171+
item={item}
172+
needsConfiguration={Boolean(unAssignedSecrets.length)}
173+
/>
174+
</Stack>
175+
</CardContent>
176+
</CardActionArea>
177+
</Card>
178+
</>
179+
);
180+
};
105181

106-
export default Tile;
182+
export default Tile;

0 commit comments

Comments
 (0)