Skip to content

🐛 Fix misleading 'No operators' empty state when fetch fails#4025

Merged
clubanderson merged 1 commit intomainfrom
fix/operator-status-empty-state
Mar 31, 2026
Merged

🐛 Fix misleading 'No operators' empty state when fetch fails#4025
clubanderson merged 1 commit intomainfrom
fix/operator-status-empty-state

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

  • Operator Status and Operator Subscription Status cards displayed "No operators" / "No Subscriptions" when the backend SSE fetch failed or the 30-second loading safety timeout fired — even though operators ARE installed on the clusters
  • Now shows an error state with a retry button when isFailed or loadingTimedOut, distinguishing "data doesn't exist" from "couldn't load data"
  • Exposes loadingTimedOut from useCardLoadingState so card components can differentiate timeout from genuine empty state

Test plan

  • Navigate to the Operators dashboard page
  • Verify Operator Status card shows operators when backend is reachable
  • Verify Operator Subscription Status card shows subscriptions when backend is reachable
  • Disconnect from clusters (or stop backend), refresh — should see error state with retry button, NOT "No operators"
  • Click retry button — should attempt to refetch

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>
Copilot AI review requested due to automatic review settings March 31, 2026 19:45
@kubestellar-prow kubestellar-prow bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Mar 31, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 31, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit 7bd7c68
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69cc24441276f000080baf82
😎 Deploy Preview https://deploy-preview-4025.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@clubanderson clubanderson merged commit b2ba58e into main Mar 31, 2026
19 of 21 checks passed
@kubestellar-prow kubestellar-prow bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Mar 31, 2026
@kubestellar-prow kubestellar-prow bot deleted the fix/operator-status-empty-state branch March 31, 2026 19:45
@github-actions
Copy link
Copy Markdown
Contributor

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 isFailed or loadingTimedOut is true.
  • Extend useCardLoadingState to return loadingTimedOut to card components.
  • Wire refetch from 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>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
<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>
)}

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +189
<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>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
<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>

Copilot uses AI. Check for mistakes.
<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
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
<button
<button
type="button"

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +147
<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>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
<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}

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +147
<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>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
<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>

Copilot uses AI. Check for mistakes.
<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
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add type="button" to this <button> to avoid implicit submit behavior if rendered inside a form.

Suggested change
<button
<button
type="button"

Copilot uses AI. Check for mistakes.
@clubanderson
Copy link
Copy Markdown
Collaborator Author

🔄 Auto-Applying Copilot Code Review

Copilot code review found 6 code suggestion(s) and 0 general comment(s).

@copilot Please apply all of the following code review suggestions:

  • web/src/components/cards/OperatorStatus.tsx (line 189): {isFailed && consecutiveFailures > 0 && ( <p className="text-xs"> ...
  • web/src/components/cards/OperatorStatus.tsx (line 189): <p className="text-sm">{t('operatorStatus.errorLoading')}</p> <p class...
  • web/src/components/cards/OperatorStatus.tsx (line 190): <button type="button"
  • web/src/components/cards/OperatorSubscriptions.tsx (line 147): <p className="text-sm"> {t('operatorSubscriptions.errorLoading', 'Un...
  • web/src/components/cards/OperatorSubscriptions.tsx (line 147): <p className="text-sm">Unable to load subscriptions</p> <p className="...
  • web/src/components/cards/OperatorSubscriptions.tsx (line 148): <button type="button"

Push all fixes in a single commit. Run cd web && npm run build && npm run lint before committing.


Auto-generated by copilot-review-apply workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants