🐛 Fix misleading 'No operators' empty state when fetch fails#4025
🐛 Fix misleading 'No operators' empty state when fetch fails#4025clubanderson merged 1 commit intomainfrom
Conversation
The Operator Status and Operator Subscription Status cards showed "No operators" / "No Subscriptions" when the SSE fetch failed or the 30-second loading timeout fired before data arrived. This was misleading because operators ARE installed — the card just couldn't fetch them. Changes: - Show error state with retry button when isFailed or loadingTimedOut instead of the misleading empty state message - Expose loadingTimedOut from useCardLoadingState so cards can differentiate between "no data exists" and "data fetch timed out" - Add refetch to both cards for manual retry Signed-off-by: Andrew Anderson <andy@clubanderson.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
✅ Deploy Preview for kubestellarconsole ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
👋 Hey @clubanderson — thanks for opening this PR!
This is an automated message. |
|
Thank you for your contribution! Your PR has been merged. Check out what's new:
Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey |
There was a problem hiding this comment.
Pull request overview
This PR improves the Operators dashboard cards’ empty/error-state behavior so they don’t misleadingly show “No operators/subscriptions” when data couldn’t be loaded (fetch failures or the loading safety timeout). It also exposes the loadingTimedOut signal from useCardLoadingState so individual cards can render a more accurate UI.
Changes:
- Add an explicit error state (with Retry) to Operator Status and Operator Subscription Status cards when
isFailedorloadingTimedOutis true. - Extend
useCardLoadingStateto returnloadingTimedOutto card components. - Wire
refetchfrom the cached data hooks into the cards’ Retry buttons.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
web/src/components/cards/OperatorStatus.tsx |
Shows an error UI + retry when cached operator fetch fails or times out, instead of default empty state. |
web/src/components/cards/OperatorSubscriptions.tsx |
Shows an error UI + retry when cached subscription fetch fails or times out, instead of default empty state. |
web/src/components/cards/CardDataContext.tsx |
Exposes loadingTimedOut from useCardLoadingState so cards can distinguish timeout from “genuinely empty”. |
| <div className="h-full flex flex-col items-center justify-center min-h-card text-muted-foreground gap-3"> | ||
| <AlertCircle className="w-8 h-8 text-red-400" /> | ||
| <p className="text-sm">{t('operatorStatus.errorLoading', 'Unable to load operators')}</p> | ||
| <p className="text-xs">{t('operatorStatus.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> |
There was a problem hiding this comment.
When loadingTimedOut triggers, consecutiveFailures is often 0 (no actual fetch failures), so this renders as "Failed after 0 attempts". Consider rendering a timeout-specific hint (e.g., "Timed out after 30s") and only showing the failure-attempts message when isFailed is true (or consecutiveFailures > 0).
| <p className="text-xs">{t('operatorStatus.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> | |
| {isFailed && consecutiveFailures > 0 && ( | |
| <p className="text-xs"> | |
| {t( | |
| 'operatorStatus.errorLoadingHint', | |
| 'Failed after {{count}} attempts', | |
| { count: consecutiveFailures }, | |
| )} | |
| </p> | |
| )} | |
| {loadingTimedOut && ( | |
| <p className="text-xs"> | |
| {t('operatorStatus.timeoutHint', 'Timed out after 30s')} | |
| </p> | |
| )} |
| <p className="text-sm">{t('operatorStatus.errorLoading', 'Unable to load operators')}</p> | ||
| <p className="text-xs">{t('operatorStatus.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> |
There was a problem hiding this comment.
New i18n keys operatorStatus.errorLoading / operatorStatus.errorLoadingHint don’t exist in the cards locale files (existing operatorStatus.* strings live under web/src/locales/*/cards.json). Please add these keys so the UI is localized instead of relying on inline English fallbacks.
| <p className="text-sm">{t('operatorStatus.errorLoading', 'Unable to load operators')}</p> | |
| <p className="text-xs">{t('operatorStatus.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> | |
| <p className="text-sm">{t('operatorStatus.errorLoading')}</p> | |
| <p className="text-xs">{t('operatorStatus.errorLoadingHint', { count: consecutiveFailures })}</p> |
| <AlertCircle className="w-8 h-8 text-red-400" /> | ||
| <p className="text-sm">{t('operatorStatus.errorLoading', 'Unable to load operators')}</p> | ||
| <p className="text-xs">{t('operatorStatus.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> | ||
| <button |
There was a problem hiding this comment.
Add type="button" to this <button> to avoid the default type="submit" behavior if this card ever ends up nested inside a <form> (prevents accidental form submission).
| <button | |
| <button | |
| type="button" |
| <p className="text-sm">{t('operatorSubscriptions.errorLoading', 'Unable to load subscriptions')}</p> | ||
| <p className="text-xs">{t('operatorSubscriptions.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> |
There was a problem hiding this comment.
When loadingTimedOut triggers, consecutiveFailures is often 0, so this renders as "Failed after 0 attempts" even though the issue was a timeout. Consider a timeout-specific hint and only show the attempts message when isFailed is true (or consecutiveFailures > 0).
| <p className="text-sm">{t('operatorSubscriptions.errorLoading', 'Unable to load subscriptions')}</p> | |
| <p className="text-xs">{t('operatorSubscriptions.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> | |
| <p className="text-sm"> | |
| {t('operatorSubscriptions.errorLoading', 'Unable to load subscriptions')} | |
| </p> | |
| {isFailed || consecutiveFailures > 0 ? ( | |
| <p className="text-xs"> | |
| {t( | |
| 'operatorSubscriptions.errorLoadingHint', | |
| 'Failed after {{count}} attempts', | |
| { count: consecutiveFailures } | |
| )} | |
| </p> | |
| ) : loadingTimedOut ? ( | |
| <p className="text-xs"> | |
| {t( | |
| 'operatorSubscriptions.timeoutHint', | |
| 'Request timed out. Data may be temporarily unavailable.' | |
| )} | |
| </p> | |
| ) : null} |
| <p className="text-sm">{t('operatorSubscriptions.errorLoading', 'Unable to load subscriptions')}</p> | ||
| <p className="text-xs">{t('operatorSubscriptions.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> |
There was a problem hiding this comment.
New i18n keys operatorSubscriptions.errorLoading / operatorSubscriptions.errorLoadingHint don’t exist in the cards locale files (see web/src/locales/*/cards.json). Please add them so non-English locales don’t regress to the fallback strings.
| <p className="text-sm">{t('operatorSubscriptions.errorLoading', 'Unable to load subscriptions')}</p> | |
| <p className="text-xs">{t('operatorSubscriptions.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> | |
| <p className="text-sm">Unable to load subscriptions</p> | |
| <p className="text-xs">Failed after {consecutiveFailures} attempts</p> |
| <AlertCircle className="w-8 h-8 text-red-400" /> | ||
| <p className="text-sm">{t('operatorSubscriptions.errorLoading', 'Unable to load subscriptions')}</p> | ||
| <p className="text-xs">{t('operatorSubscriptions.errorLoadingHint', 'Failed after {{count}} attempts', { count: consecutiveFailures })}</p> | ||
| <button |
There was a problem hiding this comment.
Add type="button" to this <button> to avoid implicit submit behavior if rendered inside a form.
| <button | |
| <button | |
| type="button" |
🔄 Auto-Applying Copilot Code ReviewCopilot code review found 6 code suggestion(s) and 0 general comment(s). @copilot Please apply all of the following code review suggestions:
Push all fixes in a single commit. Run Auto-generated by copilot-review-apply workflow. |
Summary
isFailedorloadingTimedOut, distinguishing "data doesn't exist" from "couldn't load data"loadingTimedOutfromuseCardLoadingStateso card components can differentiate timeout from genuine empty stateTest plan