Skip to content

Commit dd9801b

Browse files
author
colinmcneil
committed
Switch to react query, add caching
1 parent ccbd7f0 commit dd9801b

File tree

8 files changed

+634
-224
lines changed

8 files changed

+634
-224
lines changed

src/extension/ui/package-lock.json

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension/ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@emotion/styled": "11.10.4",
1111
"@mui/icons-material": "6.4.5",
1212
"@mui/material": "6.4.5",
13+
"@tanstack/react-query": "^5.69.0",
1314
"ansi-to-html": "^0.7.2",
1415
"json-edit-react": "^1.23.1",
1516
"react": "^18.2.0",
@@ -23,6 +24,7 @@
2324
},
2425
"devDependencies": {
2526
"@docker/extension-api-client-types": "0.3.4",
27+
"@tanstack/react-query-devtools": "^5.69.0",
2628
"@types/jest": "^29.1.2",
2729
"@types/node": "^18.7.18",
2830
"@types/react": "^18.0.17",

src/extension/ui/src/App.tsx

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Close } from '@mui/icons-material';
66
import { CatalogGrid } from './components/CatalogGrid';
77
import { POLL_INTERVAL } from './Constants';
88
import { CatalogProvider, useCatalogContext } from './context/CatalogContext';
9-
import { MCPClientProvider } from './context/MCPClientContext';
9+
import { MCPClientProvider, useMCPClientContext } from './context/MCPClientContext';
1010
import ConfigurationModal from './components/ConfigurationModal';
1111
import { Settings as SettingsIcon } from '@mui/icons-material';
1212

@@ -63,35 +63,51 @@ interface AppContentProps {
6363
}
6464

6565
function AppContent({ settings, setSettings, configuringItem, setConfiguringItem }: AppContentProps) {
66-
const { imagesLoadingResults, loadImagesIfNeeded, secrets, catalogItems, registryItems, } = useCatalogContext();
67-
if (!imagesLoadingResults || imagesLoadingResults.stderr) {
68-
return <Paper sx={{ padding: 2, height: '90vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
69-
{!imagesLoadingResults && <CircularProgress sx={{ marginBottom: 2 }} />}
70-
{!imagesLoadingResults && <Typography>Loading images...</Typography>}
71-
{imagesLoadingResults && <Alert sx={{ fontSize: '1.5em' }} action={<Button variant='outlined' color='secondary' onClick={() => loadImagesIfNeeded()}>Retry</Button>} title="Error loading images" severity="error">{imagesLoadingResults.stderr}</Alert>}
72-
<Typography>{imagesLoadingResults?.stdout}</Typography>
73-
</Paper>
74-
}
66+
const {
67+
imagesLoadingResults,
68+
loadImagesIfNeeded,
69+
catalogItems,
70+
secretsLoading,
71+
catalogLoading,
72+
registryLoading,
73+
imagesIsFetching
74+
} = useCatalogContext();
7575

76-
if (!secrets) {
77-
return <Paper sx={{ padding: 2, height: '90vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
78-
<CircularProgress />
79-
<Typography>Loading secrets...</Typography>
80-
</Paper>
81-
}
76+
const { isFetching: mcpFetching } = useMCPClientContext();
77+
78+
// Instead of showing full-page loading states for each resource, let's implement a more unified approach
79+
// Only show full-page loading during initial load, not during background refetching
80+
const isInitialLoading = !catalogItems;
8281

83-
if (!catalogItems) {
84-
return <Paper sx={{ padding: 2, height: '90vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
85-
<CircularProgress />
86-
<Typography>Loading catalog...</Typography>
87-
</Paper>
82+
// Critical error check - only for images as they're required for the app to function
83+
if (imagesLoadingResults?.stderr) {
84+
return (
85+
<Paper sx={{ padding: 2, height: '90vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
86+
<Alert
87+
sx={{ fontSize: '1.5em' }}
88+
action={
89+
<Button variant='outlined' color='secondary' onClick={() => loadImagesIfNeeded()}>
90+
Retry
91+
</Button>
92+
}
93+
title="Error loading images"
94+
severity="error"
95+
>
96+
{imagesLoadingResults.stderr}
97+
</Alert>
98+
<Typography>{imagesLoadingResults?.stdout}</Typography>
99+
</Paper>
100+
);
88101
}
89102

90-
if (!registryItems) {
91-
return <Paper sx={{ padding: 2, height: '90vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
92-
<CircularProgress />
93-
<Typography>Loading registry...</Typography>
94-
</Paper>
103+
// Show one unified loading screen during initial load
104+
if (isInitialLoading) {
105+
return (
106+
<Paper sx={{ padding: 2, height: '90vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
107+
<CircularProgress sx={{ marginBottom: 2 }} />
108+
<Typography>Loading application...</Typography>
109+
</Paper>
110+
);
95111
}
96112

97113
return (
@@ -119,7 +135,6 @@ function AppContent({ settings, setSettings, configuringItem, setConfiguringItem
119135
</Dialog>
120136
)}
121137

122-
{/* Replace the old PromptConfig dialog with our new ConfigurationModal */}
123138
{configuringItem && (
124139
<ConfigurationModal
125140
open={configuringItem !== null}
@@ -129,6 +144,27 @@ function AppContent({ settings, setSettings, configuringItem, setConfiguringItem
129144
/>
130145
)}
131146

147+
{/* Show a small loading indicator in the corner during background refetching */}
148+
{(imagesIsFetching || secretsLoading || catalogLoading || registryLoading || mcpFetching) && (
149+
<Box
150+
sx={{
151+
position: 'fixed',
152+
bottom: 16,
153+
right: 16,
154+
zIndex: 9999,
155+
display: 'flex',
156+
alignItems: 'center',
157+
backgroundColor: 'background.paper',
158+
borderRadius: 2,
159+
padding: 1,
160+
boxShadow: 3
161+
}}
162+
>
163+
<CircularProgress size={20} sx={{ mr: 1 }} />
164+
<Typography variant="caption">Refreshing data...</Typography>
165+
</Box>
166+
)}
167+
132168
<Stack direction="column" spacing={1} justifyContent='center' alignItems='center'>
133169
<Stack direction="row" spacing={1} justifyContent='space-evenly' alignItems='center' sx={{ width: '100%', maxWidth: '1000px' }}>
134170
<Logo />

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,10 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
4949
mcpClientStates,
5050
buttonsLoading,
5151
setButtonsLoading,
52-
updateMCPClientStates
52+
updateMCPClientStates,
53+
isFetching: mcpFetching
5354
} = useMCPClientContext();
5455

55-
if (!registryItems) {
56-
return <CircularProgress />
57-
}
5856

5957
const [showReloadModal, setShowReloadModal] = useState<boolean>(false);
6058
const [search, setSearch] = useState<string>('');
@@ -83,6 +81,11 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
8381
loadDDVersion();
8482
}, []);
8583

84+
85+
if (!registryItems) {
86+
return <CircularProgress />
87+
}
88+
8689
const hasOutOfCatalog = catalogItems.length > 0 && Object.keys(registryItems).length > 0 && !Object.keys(registryItems).every((i) =>
8790
catalogItems.some((c) => c.name === i)
8891
)
@@ -106,7 +109,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
106109
return <CircularProgress />
107110
}
108111

109-
const hasMCPConfigured = Object.values(mcpClientStates).some(state => state.exists && state.configured);
112+
const hasMCPConfigured = mcpFetching || Object.values(mcpClientStates || {}).some(state => state.exists && state.configured);
110113

111114
return (
112115
<Stack spacing={2} justifyContent='center' alignItems='center'>
@@ -133,7 +136,13 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
133136
}}>registry.yaml</Button>} severity="info">
134137
<Typography sx={{ width: '100%' }}>You have some prompts registered which are not available in the catalog.</Typography>
135138
</Alert>}
136-
{!hasMCPConfigured && <Alert action={<Button variant='outlined' color='secondary' onClick={showSettings}>Configure</Button>} severity="error" sx={{ fontWeight: 'bold' }}>MCP Clients are not configured. Please configure MCP Clients to use the MCP Catalog.</Alert>}
139+
{!hasMCPConfigured &&
140+
<Alert
141+
severity="error"
142+
sx={{ fontSize: '1.2em', width: '90vw', maxWidth: '1000px', mt: 2 }}>
143+
No configured clients detected. Please configure at least one client in the <strong>Clients</strong> tab.
144+
</Alert>
145+
}
137146
<Box sx={{ position: 'sticky', top: 0, zIndex: 1000, backgroundColor: 'background.default' }}>
138147
<Tabs value={tab} onChange={(_, newValue) => setTab(newValue)} sx={{ width: '90vw', maxWidth: '1000px' }}>
139148
<Tooltip title="These are all of the tiles you have available across the catalog.">
@@ -146,7 +155,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
146155
<Tab sx={{ fontSize: '1.5em' }} label="Environment" />
147156
</Tooltip>
148157
<Tooltip title="These are clients which you have configured to use your tools.">
149-
<Tab sx={{ fontSize: '1.5em' }} label="Clients" />
158+
<Tab sx={{ ...{ fontSize: '1.5em' }, ...(!hasMCPConfigured ? { color: 'docker.amber.400' } : {}) }} label="Clients" />
150159
</Tooltip>
151160
</Tabs>
152161
{tab < 2 && <Stack direction="row" spacing={1} alignItems='center' sx={{ mt: 1, py: 1 }}>
@@ -242,7 +251,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
242251
)}
243252
{tab === 3 && (
244253
<YourClients
245-
mcpClientStates={mcpClientStates}
254+
mcpClientStates={mcpClientStates || {}}
246255
onUpdate={updateMCPClientStates}
247256
setButtonsLoading={setButtonsLoading}
248257
buttonsLoading={buttonsLoading}

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

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { trackEvent } from "../../Usage";
88
import ConfigurationModal from "../ConfigurationModal";
99
import { v1 } from "@docker/extension-api-client-types";
1010
import { createDockerDesktopClient } from "@docker/extension-api-client";
11+
import { useCatalogContext } from "../../context/CatalogContext";
1112

1213
const iconSize = 16;
1314

@@ -61,14 +62,26 @@ const TileActions = ({ item, registered, register, unregister, onSecretChange, s
6162
const assignedSecrets = Secrets.getAssignedSecrets(item, secrets);
6263
setAssignedSecrets(assignedSecrets)
6364
}
65+
66+
const { registryLoading } = useCatalogContext()
67+
6468
const [isRegistering, setIsRegistering] = useState(false)
69+
const [localRegistered, setLocalRegistered] = useState(registered)
6570
const [showConfigModal, setShowConfigModal] = useState(false)
6671
const [assignedSecrets, setAssignedSecrets] = useState<{ name: string, assigned: boolean }[]>([])
6772

73+
useEffect(() => {
74+
setLocalRegistered(registered)
75+
}, [registered])
76+
6877
useEffect(() => {
6978
loadAssignedSecrets()
7079
}, [secrets])
7180

81+
if (registryLoading) {
82+
return <CircularProgress size={20} />
83+
}
84+
7285
const unAssignedSecrets = assignedSecrets.filter(s => !s.assigned)
7386

7487
const hasAllSecrets = unAssignedSecrets.length === 0
@@ -80,11 +93,6 @@ const TileActions = ({ item, registered, register, unregister, onSecretChange, s
8093
const hasAllConfig = unAssignedConfig.length === 0
8194

8295
const getActionButton = () => {
83-
if (isRegistering) {
84-
return <Tooltip title="Waiting for Docker Desktop to be ready...">
85-
<CircularProgress size={20} />
86-
</Tooltip>
87-
}
8896

8997
if (!hasAllSecrets || !hasAllConfig) {
9098
return <Stack direction="row" spacing={0} alignItems="center">
@@ -110,16 +118,28 @@ const TileActions = ({ item, registered, register, unregister, onSecretChange, s
110118
<Settings />
111119
</IconButton>
112120
</Tooltip>}
113-
<Tooltip title={registered ? "Unregistering this tile will hide it from MCP clients." : "Registering this tile will expose it to MCP clients."}>
114-
<Switch checked={registered} onChange={async (event, checked) => {
115-
setIsRegistering(true)
116-
if (checked) {
117-
await register(item)
118-
} else {
119-
await unregister(item)
120-
}
121-
setIsRegistering(false)
122-
}} />
121+
<Tooltip title={localRegistered ? "Unregistering this tile will hide it from MCP clients." : "Registering this tile will expose it to MCP clients."}>
122+
<Switch
123+
checked={localRegistered}
124+
disabled={isRegistering}
125+
onChange={async (event, checked) => {
126+
setIsRegistering(true)
127+
setLocalRegistered(checked)
128+
129+
try {
130+
if (checked) {
131+
await register(item)
132+
} else {
133+
await unregister(item)
134+
}
135+
} catch (error) {
136+
// If operation fails, revert the local state
137+
setLocalRegistered(!checked)
138+
} finally {
139+
setIsRegistering(false)
140+
}
141+
}}
142+
/>
123143
</Tooltip>
124144
</Stack>
125145
}

0 commit comments

Comments
 (0)