Skip to content

Commit 44e2811

Browse files
committed
Refactor build for smaller bundle
1 parent e4fffe9 commit 44e2811

9 files changed

+260
-98
lines changed

src/extension/ui/src/App.tsx

+29-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useState, Suspense } from 'react';
22
import { createDockerDesktopClient } from '@docker/extension-api-client';
3-
import { Stack, Typography, Button, ButtonGroup, Grid, debounce, Card, CardContent, IconButton, Alert, DialogTitle, Dialog, DialogContent, FormControlLabel, Checkbox, CircularProgress, Paper, DialogActions, Box } from '@mui/material';
3+
import { Stack, Typography, Button, IconButton, Alert, DialogTitle, Dialog, DialogContent, CircularProgress, Paper, Box } from '@mui/material';
44
import { CatalogItemWithName } from './components/PromptCard';
55
import { getRegistry, getStoredConfig, syncConfigWithRegistry, syncRegistryWithConfig } from './Registry';
6-
import { Close, FolderOpenRounded, } from '@mui/icons-material';
6+
import { Close } from '@mui/icons-material';
77
import { ExecResult } from '@docker/extension-api-client-types/dist/v0';
88
import { CatalogGrid } from './components/CatalogGrid';
99
import { POLL_INTERVAL } from './Constants';
1010
import MCPCatalogLogo from './MCP Catalog.svg'
11-
import Settings from './components/Settings';
1211
import { getMCPClientStates, MCPClientState } from './MCPClients';
1312
import PromptConfig, { ParsedParameters } from './components/PromptConfig';
1413

14+
const Settings = React.lazy(() => import('./components/Settings'));
15+
1516
export const client = createDockerDesktopClient();
1617

1718
const DEFAULT_SETTINGS = {
@@ -110,14 +111,30 @@ export function App() {
110111

111112
return (
112113
<>
113-
<Dialog open={settings.showModal} onClose={() => setSettings({ ...settings, showModal: false })} fullWidth maxWidth='md'>
114-
<DialogTitle>
115-
<Typography variant='h2' sx={{ fontWeight: 'bold', m: 2 }}>Catalog Settings</Typography>
116-
</DialogTitle>
117-
<DialogContent>
118-
<Settings onUpdate={updateMCPClientStates} mcpClientStates={mcpClientStates} settings={settings} setSettings={setSettings} />
119-
</DialogContent>
120-
</Dialog>
114+
{settings.showModal && (
115+
<Dialog open={settings.showModal} fullWidth maxWidth="xl">
116+
<DialogTitle>
117+
Settings
118+
<IconButton
119+
aria-label="close"
120+
onClick={() => setSettings({ ...settings, showModal: false })}
121+
sx={{ position: 'absolute', right: 8, top: 8 }}
122+
>
123+
<Close />
124+
</IconButton>
125+
</DialogTitle>
126+
<DialogContent>
127+
<Suspense fallback={<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>}>
128+
<Settings
129+
settings={settings}
130+
setSettings={setSettings}
131+
mcpClientStates={mcpClientStates}
132+
onUpdate={loadRegistry}
133+
/>
134+
</Suspense>
135+
</DialogContent>
136+
</Dialog>
137+
)}
121138
{configuringItem && <Dialog open={configuringItem !== null} onClose={() => setConfiguringItem(null)}>
122139
<DialogTitle>
123140
<Typography variant="h6">

src/extension/ui/src/MergeDeep.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1+
/**
2+
* Type for any object with string keys
3+
*/
4+
export type DeepObject = { [key: string]: any };
5+
16
/**
27
* Simple object check.
38
* @param item
49
* @returns {boolean}
510
*/
6-
export function isObject(item: any) {
7-
return (item && typeof item === 'object' && !Array.isArray(item));
11+
export function isObject(item: unknown): item is DeepObject {
12+
return Boolean(item && typeof item === 'object' && !Array.isArray(item));
813
}
914

1015
/**
1116
* Deep merge two objects.
12-
* @param target
13-
* @param ...sources
17+
* @param target The target object to merge into
18+
* @param sources The source objects to merge from
19+
* @returns The merged object
1420
*/
15-
export function mergeDeep(target: any, ...sources: any[]) {
21+
export function mergeDeep<T extends DeepObject>(target: T, ...sources: DeepObject[]): T {
1622
if (!sources.length) return target;
1723
const source = sources.shift();
1824

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

+38-66
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { Card, CardContent, IconButton, Alert, Stack, Button, Typography, Grid2, Select, MenuItem, FormControl, InputLabel, Switch, FormGroup, FormControlLabel, Dialog, DialogTitle, DialogContent, Checkbox, Badge, BadgeProps, Link, TextField, Tabs, Tab, Tooltip, InputAdornment, CircularProgress } from '@mui/material';
3-
import { CatalogItemWithName, CatalogItemCard, CatalogItem } from './PromptCard';
4-
import AddIcon from '@mui/icons-material/Add';
5-
import { Ref } from '../Refs';
1+
import React, { Suspense, useEffect, useState } from 'react';
2+
import { IconButton, Alert, Stack, Button, Typography, FormGroup, FormControlLabel, Dialog, DialogTitle, DialogContent, Checkbox, Badge, BadgeProps, Link, TextField, Tabs, Tab, Tooltip, CircularProgress, Box } from '@mui/material';
3+
import { CatalogItemWithName, CatalogItem } from './PromptCard';
64
import { v1 } from "@docker/extension-api-client-types";
75
import { parse, stringify } from 'yaml';
86
import { getRegistry, syncConfigWithRegistry, syncRegistryWithConfig } from '../Registry';
97
import { FolderOpenRounded, Search, Settings } from '@mui/icons-material';
108
import { tryRunImageSync } from '../FileWatcher';
11-
import { CATALOG_URL, DD_BUILD_WITH_SECRET_SUPPORT, MCP_POLICY_NAME, POLL_INTERVAL } from '../Constants';
12-
import { SecretList } from './SecretList';
9+
import { CATALOG_URL, POLL_INTERVAL } from '../Constants';
1310
import Secrets from '../Secrets';
1411
import { ParsedParameters } from './PromptConfig';
1512

13+
const ToolCatalog = React.lazy(() => import('./tabs/ToolCatalog'));
14+
const YourTools = React.lazy(() => import('./tabs/YourTools'));
15+
const YourEnvironment = React.lazy(() => import('./tabs/YourEnvironment'));
16+
1617
interface CatalogGridProps {
1718
registryItems: { [key: string]: { ref: string, config: any } };
1819
canRegister: boolean;
@@ -184,7 +185,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
184185
}}>registry.yaml</Button>} severity="info">
185186
<Typography sx={{ width: '100%' }}>You have some prompts registered which are not available in the catalog.</Typography>
186187
</Alert>}
187-
<Tabs value={tab} onChange={(e, v) => setTab(v)} sx={{ mb: 0, mt: 1 }}>
188+
<Tabs value={tab} onChange={(_, newValue) => setTab(newValue)} sx={{ width: '100%' }}>
188189
<Tooltip title="These are all of the tiles you have available across the catalog.">
189190
<Tab sx={{ fontSize: '1.5em' }} label="Tool Catalog" />
190191
</Tooltip>
@@ -210,63 +211,34 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
210211
</Badge>
211212
</IconButton>
212213
</Stack>
213-
</FormGroup >
214-
215-
{tab === 0 && <Grid2 container spacing={1} width='90vw' maxWidth={1000}>
216-
{filteredCatalogItems.map((catalogItem) => {
217-
const expectedProperties = catalogItem.config?.map((c: { name: string, parameters: ParsedParameters }) => Object.keys(c.parameters)).flat() || []
218-
const hasAllConfig = !expectedProperties.length || expectedProperties.every((p: string) => config[catalogItem.name]?.[p])
219-
return (
220-
<Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={catalogItem.name}>
221-
<CatalogItemCard
222-
hasAllConfig={hasAllConfig}
223-
setConfiguringItem={setConfiguringItem}
224-
openUrl={() => {
225-
client.host.openExternal(Ref.fromRef(catalogItem.ref).toURL(true));
226-
}}
227-
item={catalogItem}
228-
ddVersion={ddVersion}
229-
canRegister={canRegister}
230-
registered={Object.keys(registryItems).some((i) => i === catalogItem.name)}
231-
register={registerCatalogItem}
232-
unregister={unregisterCatalogItem}
233-
onSecretChange={async (secret) => {
234-
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
235-
loadSecrets();
236-
}}
237-
secrets={secrets}
238-
/>
239-
</Grid2>
240-
)
241-
})}
242-
<Grid2 size={12}>
243-
<Card sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
244-
<CardContent>
245-
<IconButton sx={{ height: '100%' }} onClick={() => {
246-
client.host.openExternal('https://vonwig.github.io/prompts.docs/tools/docs/');
247-
}}>
248-
<AddIcon sx={{ width: '100%', height: 100 }} />
249-
</IconButton>
250-
</CardContent>
251-
</Card>
252-
</Grid2>
253-
</Grid2>}
254-
{tab === 1 && <Grid2 container spacing={1} width='90vw' maxWidth={1000}>
255-
{Object.entries(registryItems).map(([name, item]) => {
256-
const hasAllConfig = item.config?.map((c: any) => c.name).every((c: any) => config[name]?.[c])
257-
return (
258-
name.toLowerCase().includes(search.toLowerCase()) && <Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={name}>
259-
<CatalogItemCard hasAllConfig={hasAllConfig} ddVersion={ddVersion} item={catalogItems.find((i) => i.name === name)!} openUrl={() => {
260-
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
261-
}} canRegister={canRegister} registered={true} register={registerCatalogItem} unregister={unregisterCatalogItem} onSecretChange={async (secret) => {
262-
await Secrets.addSecret(client, { name: secret.name, value: secret.value, policies: [MCP_POLICY_NAME] })
263-
loadSecrets();
264-
}} secrets={secrets} setConfiguringItem={setConfiguringItem} />
265-
</Grid2>
266-
)
267-
})}
268-
</Grid2>}
269-
{tab === 2 && ddVersion && <SecretList secrets={secrets} ddVersion={ddVersion} />}
270-
</Stack >
214+
</FormGroup>
215+
216+
<Suspense fallback={<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>}>
217+
{tab === 0 && (
218+
<ToolCatalog
219+
search={search}
220+
catalogItems={catalogItems}
221+
client={client}
222+
/>
223+
)}
224+
{tab === 1 && (
225+
<YourTools
226+
search={search}
227+
registryItems={registryItems}
228+
config={config}
229+
ddVersion={ddVersion}
230+
canRegister={canRegister}
231+
setConfiguringItem={setConfiguringItem}
232+
secrets={secrets}
233+
/>
234+
)}
235+
{tab === 2 && ddVersion && (
236+
<YourEnvironment
237+
secrets={secrets}
238+
ddVersion={ddVersion}
239+
/>
240+
)}
241+
</Suspense>
242+
</Stack>
271243
);
272244
};

src/extension/ui/src/components/PromptCard.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import { Badge, Box, CircularProgress, Dialog, DialogContent, DialogTitle, Divider, IconButton, List, ListItem, ListItemIcon, ListItemText, Stack, Switch, TextField, Tooltip, useTheme } from "@mui/material";
2-
import Button from '@mui/material/Button';
32
import { Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material";
4-
import { Ref } from "../Refs";
53
import { useEffect, useState } from "react";
64
import { trackEvent } from "../Usage";
7-
import { Article, AttachFile, Build, CheckBox, Delete, DoNotDisturb, LockOpenRounded, LockReset, LockRounded, NoEncryptionGmailerrorred, Save, Settings } from "@mui/icons-material";
5+
import { Article, AttachFile, Build, DoNotDisturb, LockReset, LockRounded, NoEncryptionGmailerrorred, Save, Settings } from "@mui/icons-material";
86
import Secrets from "../Secrets";
97
import { DD_BUILD_WITH_SECRET_SUPPORT, getUnsupportedSecretMessage } from "../Constants";
10-
import { DataType, githubDarkTheme, githubLightTheme, JsonEditor, NodeData } from "json-edit-react";
11-
import PromptConfig, { Config } from "./PromptConfig";
12-
const iconSize = 16
13-
8+
import { Config } from "./PromptConfig";
149

10+
const iconSize = 16
1511

1612
export interface CatalogItem {
1713
description?: string;

src/extension/ui/src/components/PromptConfig.tsx

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import { githubLightTheme, NodeData } from "json-edit-react"
2-
3-
import { githubDarkTheme } from "json-edit-react"
4-
1+
import { githubLightTheme, NodeData, githubDarkTheme, JsonEditor } from "json-edit-react"
52
import { CircularProgress, useTheme } from "@mui/material";
6-
import { JsonEditor } from "json-edit-react"
73
import { useEffect, useState } from "react";
84
import { CatalogItemWithName } from "./PromptCard";
9-
import { getRegistry, getStoredConfig } from "../Registry";
5+
import { getStoredConfig } from "../Registry";
106
import { v1 } from "@docker/extension-api-client-types";
11-
import { mergeDeep } from "../MergeDeep";
7+
import { DeepObject, mergeDeep } from "../MergeDeep";
128
import { stringify } from "yaml";
139
import { tryRunImageSync } from "../FileWatcher";
1410

@@ -158,7 +154,7 @@ const PromptConfig = ({
158154
key={config.name}
159155
theme={theme.palette.mode === 'dark' ? githubDarkTheme : githubLightTheme}
160156
onEdit={({ newData }) => {
161-
const newConfig = mergeDeep(existingConfigInYaml, newData)
157+
const newConfig = mergeDeep(existingConfigInYaml as DeepObject, newData as DeepObject)
162158
saveConfigToYaml(newConfig)
163159
}}
164160
rootName={config.name}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import { Card, CardContent, Grid2, IconButton } from '@mui/material';
3+
import { CatalogItemCard, CatalogItemWithName } from '../PromptCard';
4+
import AddIcon from '@mui/icons-material/Add';
5+
import { Ref } from '../../Refs';
6+
import { v1 } from "@docker/extension-api-client-types";
7+
8+
interface ToolCatalogProps {
9+
search: string;
10+
catalogItems: CatalogItemWithName[];
11+
client: v1.DockerDesktopClient;
12+
}
13+
14+
const ToolCatalog: React.FC<ToolCatalogProps> = ({ search, catalogItems, client }) => {
15+
const filteredCatalogItems = catalogItems.filter(item =>
16+
item.name.toLowerCase().includes(search.toLowerCase())
17+
);
18+
19+
return (
20+
<Grid2 container spacing={1} width='90vw' maxWidth={1000}>
21+
{filteredCatalogItems.map((catalogItem) => (
22+
<Grid2 size={{ xs: 12, sm: 6, md: 4 }} key={catalogItem.name}>
23+
<CatalogItemCard
24+
hasAllConfig={true}
25+
setConfiguringItem={() => { }}
26+
openUrl={() => {
27+
client.host.openExternal(Ref.fromRef(catalogItem.ref).toURL(true));
28+
}}
29+
item={catalogItem}
30+
ddVersion={{ version: '0.0.0', build: 0 }}
31+
canRegister={false}
32+
registered={false}
33+
register={async () => { }}
34+
unregister={async () => { }}
35+
onSecretChange={async () => { }}
36+
secrets={[]}
37+
/>
38+
</Grid2>
39+
))}
40+
<Grid2 size={12}>
41+
<Card sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
42+
<CardContent>
43+
<IconButton sx={{ height: '100%' }} onClick={() => {
44+
client.host.openExternal('https://vonwig.github.io/prompts.docs/tools/docs/');
45+
}}>
46+
<AddIcon sx={{ width: '100%', height: 100 }} />
47+
</IconButton>
48+
</CardContent>
49+
</Card>
50+
</Grid2>
51+
</Grid2>
52+
);
53+
};
54+
55+
export default ToolCatalog;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import { Alert, List, ListItem, ListItemText, Stack, Typography } from '@mui/material';
3+
import Secrets from '../../Secrets';
4+
import { DD_BUILD_WITH_SECRET_SUPPORT, getUnsupportedSecretMessage } from '../../Constants';
5+
6+
interface YourEnvironmentProps {
7+
secrets: Secrets.Secret[];
8+
ddVersion: { version: string, build: number };
9+
}
10+
11+
const YourEnvironment: React.FC<YourEnvironmentProps> = ({ secrets, ddVersion }) => {
12+
const hasDDVersionWithSecretSupport = ddVersion && ddVersion.build >= DD_BUILD_WITH_SECRET_SUPPORT;
13+
14+
if (!hasDDVersionWithSecretSupport) {
15+
return <Alert severity="error" sx={{ fontSize: '1.2rem', maxWidth: 600 }}>{getUnsupportedSecretMessage(ddVersion)}</Alert>
16+
}
17+
18+
return (
19+
<List subheader={
20+
<Stack direction="column" spacing={2} alignItems="center">
21+
<Alert title="Docker Secret Management" severity="info" sx={{ fontSize: '1.2rem', maxWidth: 600 }}>
22+
Docker Secret Management is a new feature in Docker Desktop that allows you to securely inject secrets into your containers.
23+
</Alert>
24+
<Typography variant="h2">
25+
The following secrets are available to use in your prompts:
26+
</Typography>
27+
</Stack>
28+
}>
29+
{secrets.map((secret) => (
30+
<ListItem key={secret.name}>
31+
<ListItemText primary={<Typography variant="h6">{secret.name}</Typography>} />
32+
</ListItem>
33+
))}
34+
</List>
35+
);
36+
};
37+
38+
export default YourEnvironment;

0 commit comments

Comments
 (0)