Skip to content

Commit da48d4f

Browse files
feat(portal): improve component creation error handling and UX (#73)
- Show clear warning when environment has no clusters - Prevent auto-redirect on component creation errors - Display actionable error messages with options to navigate or fix - Add proactive validation to block component creation without clusters - Improve empty state message when no clusters are available - Add cluster validation alerts in component creation modals This improves user experience by: - Making it clear why components can't be created - Providing guidance on how to fix the issue (add cluster) - Not hiding errors with automatic redirects - Showing warnings before users attempt to create components Co-authored-by: rafaelrsantosti <11065120+rafaelrsantosti@users.noreply.github.com>
1 parent 1dca7d8 commit da48d4f

2 files changed

Lines changed: 127 additions & 43 deletions

File tree

portal/src/pages/applications/CreateApplication.tsx

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function CreateApplication() {
2727
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null)
2828
const [, setErrors] = useState<Record<string, string>>({})
2929
const [isCreating, setIsCreating] = useState(false)
30+
const [partialSuccess, setPartialSuccess] = useState<{ applicationUuid: string; instanceUuid: string } | null>(null)
3031
const { user } = useAuth()
3132
const isAdmin = user?.role === 'admin'
3233

@@ -55,36 +56,37 @@ function CreateApplication() {
5556
enabled: true,
5657
})
5758

59+
// Check if environment has any clusters
60+
const environmentClusters = useMemo(() => {
61+
if (!instanceData.environment_uuid || !clusters) return []
62+
return clusters.filter(
63+
(cluster) => cluster.environment?.uuid === instanceData.environment_uuid
64+
)
65+
}, [instanceData.environment_uuid, clusters])
66+
67+
const hasNoClusters = instanceData.environment_uuid && environmentClusters.length === 0
68+
5869
// Check if any cluster in the selected environment has gateway_api available
5970
const hasGatewayApi = useMemo(() => {
6071
if (!instanceData.environment_uuid || !clusters) return false
61-
const environmentClusters = clusters.filter(
62-
(cluster) => cluster.environment?.uuid === instanceData.environment_uuid
63-
)
6472
return environmentClusters.some((cluster) => cluster.gateway?.api?.enabled === true)
65-
}, [instanceData.environment_uuid, clusters])
73+
}, [instanceData.environment_uuid, clusters, environmentClusters])
6674

6775
// Get Gateway API resources available in clusters of the selected environment
6876
const gatewayResources = useMemo(() => {
69-
if (!instanceData.environment_uuid || !clusters) return []
70-
const environmentClusters = clusters.filter(
71-
(cluster) => cluster.environment?.uuid === instanceData.environment_uuid
72-
)
77+
if (environmentClusters.length === 0) return []
7378
const allResources = new Set<string>()
7479
environmentClusters.forEach((cluster) => {
7580
if (cluster.gateway?.api?.enabled && cluster.gateway.api.resources) {
7681
cluster.gateway.api.resources.forEach((resource) => allResources.add(resource))
7782
}
7883
})
7984
return Array.from(allResources)
80-
}, [instanceData.environment_uuid, clusters])
85+
}, [environmentClusters])
8186

8287
// Get Gateway reference from clusters of the selected environment
8388
const gatewayReference = useMemo(() => {
84-
if (!instanceData.environment_uuid || !clusters) return { namespace: '', name: '' }
85-
const environmentClusters = clusters.filter(
86-
(cluster) => cluster.environment?.uuid === instanceData.environment_uuid
87-
)
89+
if (environmentClusters.length === 0) return { namespace: '', name: '' }
8890
for (const cluster of environmentClusters) {
8991
if (cluster.gateway?.reference) {
9092
const ref = cluster.gateway.reference.private || cluster.gateway.reference.public || { namespace: '', name: '' }
@@ -96,7 +98,7 @@ function CreateApplication() {
9698
}
9799
}
98100
return { namespace: '', name: '' }
99-
}, [instanceData.environment_uuid, clusters])
101+
}, [environmentClusters])
100102

101103
// Components
102104
const [components, setComponents] = useState<ComponentFormData[]>([])
@@ -228,6 +230,15 @@ function CreateApplication() {
228230
return
229231
}
230232

233+
// Check if environment has clusters
234+
if (hasNoClusters) {
235+
setNotification({
236+
type: 'error',
237+
message: 'The selected environment has no clusters. Please add a cluster in Settings → Clusters before creating components.',
238+
})
239+
return
240+
}
241+
231242
setIsCreating(true)
232243

233244
let application: { uuid: string } | null = null
@@ -297,34 +308,31 @@ function CreateApplication() {
297308
// eslint-disable-next-line @typescript-eslint/no-explicit-any
298309
} catch (error: any) {
299310
const errorMessage = error.response?.data?.detail || error.message || 'Error creating application'
311+
setIsCreating(false)
300312

301-
// If application and instance were created, redirect anyway (partial success)
313+
// If application and instance were created, show error but allow user to decide
302314
if (application && instance) {
315+
setPartialSuccess({ applicationUuid: application.uuid, instanceUuid: instance.uuid })
303316
setNotification({
304317
type: 'error',
305-
message: `Application created but there was an error with components: ${errorMessage}. Redirecting...`,
318+
message: `Application and instance created, but component failed: ${errorMessage}`,
306319
})
307-
setTimeout(() => {
308-
navigate(`/applications/${application!.uuid}/instances/${instance!.uuid}/components`)
309-
}, 2000)
320+
// Show a button to navigate or let user fix the issue and retry
321+
// Don't auto-redirect - let user see the error and decide
310322
} else if (application) {
311323
// Application created but instance failed
312324
setNotification({
313325
type: 'error',
314-
message: `Application created but instance failed: ${errorMessage}. Redirecting...`,
326+
message: `Application created but instance failed: ${errorMessage}`,
315327
})
316-
setTimeout(() => {
317-
navigate(`/applications/${application!.uuid}`)
318-
}, 2000)
319328
} else {
320329
// Complete failure - allow retry
321-
setIsCreating(false)
322330
setNotification({
323331
type: 'error',
324332
message: errorMessage,
325333
})
326-
setTimeout(() => setNotification(null), 5000)
327334
}
335+
// Don't auto-hide error notifications - let user dismiss them
328336
}
329337
}
330338

@@ -360,7 +368,23 @@ function CreateApplication() {
360368
)
361369

362370
const step2Content = (
363-
<InstanceForm data={instanceData} onChange={setInstanceData} showInfoCard={false} />
371+
<div className="space-y-4">
372+
<InstanceForm data={instanceData} onChange={setInstanceData} showInfoCard={false} />
373+
{hasNoClusters && (
374+
<div className="p-4 rounded-lg bg-amber-50 border border-amber-200 text-amber-800">
375+
<div className="flex items-start gap-2">
376+
<span className="text-amber-500 mt-0.5">⚠️</span>
377+
<div>
378+
<p className="font-medium">No clusters in this environment</p>
379+
<p className="text-sm mt-1">
380+
This environment has no clusters configured. You won't be able to deploy components until a cluster is added.
381+
Go to <span className="font-medium">Settings → Clusters</span> to add one.
382+
</p>
383+
</div>
384+
</div>
385+
</div>
386+
)}
387+
</div>
364388
)
365389

366390
const step3Content = (
@@ -513,7 +537,42 @@ function CreateApplication() {
513537
notification.type === 'success' ? 'bg-green-50 text-green-800 border border-green-200' : 'bg-red-50 text-red-800 border border-red-200'
514538
}`}
515539
>
516-
{notification.message}
540+
<div className="flex flex-col gap-3">
541+
<div className="flex items-start justify-between">
542+
<span>{notification.message}</span>
543+
<button
544+
type="button"
545+
onClick={() => {
546+
setNotification(null)
547+
if (!partialSuccess) setPartialSuccess(null)
548+
}}
549+
className="text-slate-400 hover:text-slate-600 ml-2"
550+
>
551+
552+
</button>
553+
</div>
554+
{partialSuccess && notification.type === 'error' && (
555+
<div className="flex gap-2 mt-2">
556+
<button
557+
type="button"
558+
onClick={() => navigate(`/applications/${partialSuccess.applicationUuid}/instances/${partialSuccess.instanceUuid}/components`)}
559+
className="px-3 py-1.5 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
560+
>
561+
Go to Instance (add components there)
562+
</button>
563+
<button
564+
type="button"
565+
onClick={() => {
566+
setNotification(null)
567+
setPartialSuccess(null)
568+
}}
569+
className="px-3 py-1.5 text-sm bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors"
570+
>
571+
Stay and fix
572+
</button>
573+
</div>
574+
)}
575+
</div>
517576
</div>
518577
)}
519578

portal/src/pages/applications/InstanceDetail.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,24 @@ function InstanceDetail() {
3535
queryFn: () => clustersApi.list(),
3636
})
3737

38-
// Check if any cluster in the instance's environment has gateway_api available
39-
const hasGatewayApi = useMemo(() => {
40-
if (!instance || !clusters || !instance.environment) return false
41-
const environmentClusters = clusters.filter(
38+
// Get clusters in the instance's environment
39+
const environmentClusters = useMemo(() => {
40+
if (!instance || !clusters || !instance.environment) return []
41+
return clusters.filter(
4242
(cluster) => cluster.environment?.uuid === instance.environment.uuid
4343
)
44-
return environmentClusters.some((cluster) => cluster.gateway?.api?.enabled === true)
4544
}, [instance, clusters])
4645

46+
const hasNoClusters = instance?.environment && environmentClusters.length === 0
47+
48+
// Check if any cluster in the instance's environment has gateway_api available
49+
const hasGatewayApi = useMemo(() => {
50+
return environmentClusters.some((cluster) => cluster.gateway?.api?.enabled === true)
51+
}, [environmentClusters])
52+
4753
// Get Gateway API resources available in environment clusters
4854
const gatewayResources = useMemo(() => {
49-
if (!instance || !clusters || !instance.environment) return []
50-
const environmentClusters = clusters.filter(
51-
(cluster) => cluster.environment?.uuid === instance.environment.uuid
52-
)
55+
if (environmentClusters.length === 0) return []
5356
// Get resources from all clusters that have Gateway API enabled
5457
const allResources = new Set<string>()
5558
environmentClusters.forEach((cluster) => {
@@ -58,14 +61,11 @@ function InstanceDetail() {
5861
}
5962
})
6063
return Array.from(allResources)
61-
}, [instance, clusters])
64+
}, [environmentClusters])
6265

6366
// Get Gateway reference (namespace and name) from environment clusters
6467
const gatewayReference = useMemo(() => {
65-
if (!instance || !clusters || !instance.environment) return { namespace: '', name: '' }
66-
const environmentClusters = clusters.filter(
67-
(cluster) => cluster.environment?.uuid === instance.environment.uuid
68-
)
68+
if (environmentClusters.length === 0) return { namespace: '', name: '' }
6969
// Get the first gateway reference found that has namespace and name filled
7070
// Use private gateway as default reference (both public and private use same auto-discovery if not configured)
7171
for (const cluster of environmentClusters) {
@@ -79,7 +79,7 @@ function InstanceDetail() {
7979
}
8080
}
8181
return { namespace: '', name: '' }
82-
}, [instance, clusters])
82+
}, [environmentClusters])
8383

8484
const [editingComponentUuid, setEditingComponentUuid] = useState<string | null>(null)
8585
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null)
@@ -916,7 +916,19 @@ function InstanceDetail() {
916916

917917
{Object.values(componentsByType).every((components) => components.length === 0) && (
918918
<div className="bg-white rounded-xl shadow-soft border border-slate-200/60 p-12 text-center">
919-
<p className="text-slate-500 text-lg">No components found. Click "Add Component" to get started.</p>
919+
{hasNoClusters ? (
920+
<div className="space-y-3">
921+
<p className="text-amber-600 text-lg font-medium">⚠️ No clusters in this environment</p>
922+
<p className="text-slate-500">
923+
You need to add a cluster to the <span className="font-medium">{instance?.environment?.name}</span> environment before you can create components.
924+
</p>
925+
<p className="text-sm text-slate-400">
926+
Go to <span className="font-medium">Settings → Clusters</span> to add one.
927+
</p>
928+
</div>
929+
) : (
930+
<p className="text-slate-500 text-lg">No components found. Click "Add Component" to get started.</p>
931+
)}
920932
</div>
921933
)}
922934
</div>
@@ -964,6 +976,19 @@ function InstanceDetail() {
964976
</button>
965977
</div>
966978
)}
979+
{hasNoClusters && !editingComponentUuid && (
980+
<div className="mb-4 p-4 rounded-lg bg-amber-50 border border-amber-200 text-amber-800">
981+
<div className="flex items-start gap-2">
982+
<span className="text-amber-500 mt-0.5">⚠️</span>
983+
<div>
984+
<p className="font-medium text-sm">No clusters in this environment</p>
985+
<p className="text-sm mt-1">
986+
Components cannot be deployed without a cluster. Go to <span className="font-medium">Settings → Clusters</span> to add one.
987+
</p>
988+
</div>
989+
</div>
990+
</div>
991+
)}
967992
{component ? (
968993
<ComponentForm
969994
component={component}

0 commit comments

Comments
 (0)