Skip to content

InfoTab: Add caching for namespace details#503

Draft
skoeva wants to merge 1 commit intoAzure:mainfrom
skoeva:infotab
Draft

InfoTab: Add caching for namespace details#503
skoeva wants to merge 1 commit intoAzure:mainfrom
skoeva:infotab

Conversation

@skoeva
Copy link
Copy Markdown
Collaborator

@skoeva skoeva commented Mar 21, 2026

The InfoTab was making two sequential Azure CLI calls on every open, causing ~1 minute load times. This change replaces that with a single fetch backed by a module-level cache so subsequent opens are instant.

Fixes: #280

Summary

  • Added a module-level detailsCache (stale-while-revalidate): cached data is shown immediately, a background fetch keeps it fresh. Cache key includes subscription/resourceGroup/clusterName/projectId to prevent cross-subscription collisions
  • Revalidation is based on whether data is already displayed (not cache presence), so background fetches are used even after a save invalidates the cache — no cold-start spinner after saving
  • Cache is invalidated after a successful save so stale data is never shown post-update
  • handleRefresh triggers a background revalidation without clearing the cache, keeping the form visible
  • Added revalidating state for background fetch indication
  • Updated InfoTab.tsx to show a background spinner and a Refresh button
  • Updated tests: removed obsolete getManagedNamespaces mock/cases; added coverage for cache hit, background revalidation, error suppression with existing data, cache invalidation on save, and post-save refresh running as revalidation; replaced direct detailsCache access with exported test helpers

Testing

  • Run cd plugins/aks-desktop && npm test and ensure the tests pass

@skoeva skoeva self-assigned this Mar 21, 2026
@skoeva skoeva added the bug Something isn't working label Mar 21, 2026
@skoeva skoeva force-pushed the infotab branch 2 times, most recently from 510e14b to fdc2582 Compare March 25, 2026 18:41
@skoeva skoeva marked this pull request as ready for review March 25, 2026 20:23
Copilot AI review requested due to automatic review settings March 25, 2026 20:23

This comment was marked as outdated.

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings March 31, 2026 19:33
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

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.ts:235

  • The namespaceDetails -> form pre-population effect runs on every namespaceDetails change and unconditionally calls setFormData/setBaselineFormData. With the new background revalidation behavior, a late-arriving refresh can overwrite in-progress user edits (and reset hasChanges) while the user is typing. Guard this so background revalidation only updates the form when there are no unsaved changes (or only populate on the initial load), otherwise keep the user’s local edits and optionally signal that newer server data is available.
  // Pre-populate form when namespace details are fetched
  useEffect(() => {
    if (!namespaceDetails) return;

    const quota = namespaceDetails.properties?.defaultResourceQuota;
    const policy = namespaceDetails.properties?.defaultNetworkPolicy;

    const populated: FormData = {
      ...DEFAULT_FORM_DATA,
      ingress: normalizePolicy(policy?.ingress ?? 'AllowSameNamespace'),
      egress: normalizePolicy(policy?.egress ?? 'AllowAll'),
      cpuRequest: parseMillicores(quota?.cpuRequest ?? '0m'),
      cpuLimit: parseMillicores(quota?.cpuLimit ?? '0m'),
      memoryRequest: parseMiB(quota?.memoryRequest ?? '0Mi'),
      memoryLimit: parseMiB(quota?.memoryLimit ?? '0Mi'),
    };

    setFormData(populated);
    setBaselineFormData(populated);
  }, [namespaceDetails]);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.ts
Comment thread plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.test.ts
@skoeva skoeva marked this pull request as draft March 31, 2026 21:12
@skoeva skoeva marked this pull request as ready for review April 1, 2026 13:07
Copilot AI review requested due to automatic review settings April 1, 2026 13:07
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

Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.ts Outdated
Comment thread plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.ts Outdated
Comment thread plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.ts
Comment thread plugins/aks-desktop/src/components/InfoTab/hooks/useInfoTab.ts Outdated
tejhan
tejhan previously approved these changes Apr 6, 2026
Copy link
Copy Markdown
Collaborator

@tejhan tejhan left a comment

Choose a reason for hiding this comment

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

Changes LGTM, tests pass & was able to replicated new behavior.

@illume illume requested a review from Copilot April 8, 2026 09:05
Copy link
Copy Markdown
Collaborator

@illume illume left a comment

Choose a reason for hiding this comment

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

@skoeva it seems there's a git conflict now. Can you please take a look?

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

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +164 to +177
// Skip the initial mount — state is already initialised correctly from the cache.
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
return;
}
setNamespaceDetails(null);
setFormData(DEFAULT_FORM_DATA);
setBaselineFormData(null);
hasUnsavedChangesRef.current = false;
setError(null);
setLoading(!!clusterName);
setRevalidating(false);
}, [clusterName, projectId]);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The “project identity reset” effect only keys off [clusterName, projectId], but the cache key includes subscription and resourceGroup. If those labels change without clusterName/projectId changing, the hook can temporarily keep showing the previous project’s namespaceDetails while revalidating the new cacheKey. Consider basing the reset on cacheKey (or including subscription/resourceGroup) and, on reset, seeding namespaceDetails/loading from detailsCache.get(nextCacheKey) so cached data can be shown immediately on project switches (consistent with the SWR intent).

Suggested change
// Skip the initial mount — state is already initialised correctly from the cache.
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
return;
}
setNamespaceDetails(null);
setFormData(DEFAULT_FORM_DATA);
setBaselineFormData(null);
hasUnsavedChangesRef.current = false;
setError(null);
setLoading(!!clusterName);
setRevalidating(false);
}, [clusterName, projectId]);
// Use the same identity as the cache/fetch path, and hydrate from cache
// immediately when available so project switches keep SWR behaviour.
// Skip the initial mount — state is already initialised correctly from the cache.
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
return;
}
const cachedDetails = cacheKey ? detailsCache.get(cacheKey) ?? null : null;
setNamespaceDetails(cachedDetails);
setFormData(DEFAULT_FORM_DATA);
setBaselineFormData(null);
hasUnsavedChangesRef.current = false;
setError(null);
setLoading(!!cacheKey && !cachedDetails);
setRevalidating(!!cacheKey && !!cachedDetails);
}, [cacheKey, clusterName, projectId]);

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +111
<Tooltip title={t('Refreshing data in background')}>
<CircularProgress size={16} />
</Tooltip>
)}
<Button
variant="outlined"
onClick={handleRefresh}
disabled={revalidating || updating}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The background revalidation spinner is rendered as a standalone CircularProgress with only a hover Tooltip. This isn’t reliably announced to screen readers and doesn’t convey why the Refresh button becomes disabled. Consider marking the spinner aria-hidden and adding an accessible busy/status signal (e.g., aria-busy={revalidating} on the Refresh button and/or a visually-hidden role="status" live region with the same text).

Suggested change
<Tooltip title={t('Refreshing data in background')}>
<CircularProgress size={16} />
</Tooltip>
)}
<Button
variant="outlined"
onClick={handleRefresh}
disabled={revalidating || updating}
<>
<Box
id="info-tab-refresh-status"
role="status"
aria-live="polite"
sx={{
position: 'absolute',
width: 1,
height: 1,
p: 0,
m: -1,
overflow: 'hidden',
clip: 'rect(0 0 0 0)',
whiteSpace: 'nowrap',
border: 0,
}}
>
{t('Refreshing data in background')}
</Box>
<Tooltip title={t('Refreshing data in background')}>
<CircularProgress size={16} aria-hidden="true" />
</Tooltip>
</>
)}
<Button
variant="outlined"
onClick={handleRefresh}
disabled={revalidating || updating}
aria-busy={revalidating}
aria-describedby={revalidating ? 'info-tab-refresh-status' : undefined}

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

Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

Localize/locales/en/plugin-translation.json:175

  • "Cores" was removed from the localization source, but it is still referenced in the plugin UI (e.g. plugins/aks-desktop/src/components/Metrics/components/MetricsChartsGrid.tsx uses t('Cores')). Please restore this key (or move it to the correct localization source) to avoid missing translations.
  "Copilot agent": "Copilot agent",
  "Copilot agent reviewed your repo and generated deployment PR": "Copilot agent reviewed your repo and generated deployment PR",
  "Copy": "Copy",
  "Copy to clipboard": "Copy to clipboard",
  "CPU": "CPU",
  "CPU limit": "CPU limit",
  "CPU Limits": "CPU Limits",
  "CPU request": "CPU request",
  "CPU Requests": "CPU Requests",
  "CPU Resources": "CPU Resources",

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Localize/locales/en/plugin-translation.json
Comment thread Localize/locales/en/plugin-translation.json
Comment thread Localize/locales/en/plugin-translation.json
Signed-off-by: Evangelos Skopelitis <eskopelitis@microsoft.com>
Copy link
Copy Markdown
Collaborator

@illume illume left a comment

Choose a reason for hiding this comment

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

There's some git conflicts now. Can you please have a look?

(Also some review comment(s) left open)

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

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

>
{revalidating && (
<Tooltip title={t('Refreshing data in background')}>
<CircularProgress size={16} />
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

The background refresh indicator is a bare CircularProgress wrapped in a Tooltip. Tooltips on non-focusable elements aren’t keyboard-accessible, and CircularProgress may be exposed to screen readers as an unlabeled progressbar. Consider either marking it aria-hidden (if decorative) and adding separate accessible text, or give it an aria-label/aria-describedby so assistive tech can understand what’s happening.

Suggested change
<CircularProgress size={16} />
<CircularProgress
size={16}
aria-label={t('Refreshing data in background')}
/>

Copilot uses AI. Check for mistakes.

await waitFor(() => expect(result.current.revalidating).toBe(false));
});

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

There’s good coverage for cache-hit and revalidation, but no test covers the case where Namespace.useGet initially returns no labels (so cacheKey is null) and then later provides subscription/resourceGroup. That’s the scenario where state hydration from the module cache can be missed. Consider adding a test that starts with missing labels + a pre-seeded cache, then updates the mock to return labels and asserts cached data is shown immediately (no loading spinner) and the fetch runs as revalidating.

Suggested change
test('hydrates from cache when labels become available after initial render and revalidates in background', async () => {
const cached = createNamespaceDetails({ ingress: 'AllowAll' });
setDetailsCacheForTests('sub-123/rg-prod/my-cluster/my-project', cached);
let resolveFetch: (value: ReturnType<typeof createNamespaceDetails>) => void;
const fetchPromise = new Promise<ReturnType<typeof createNamespaceDetails>>(resolve => {
resolveFetch = resolve;
});
mockGetManagedNamespaceDetails.mockReturnValue(fetchPromise);
mockUseGet
.mockReturnValueOnce({
jsonData: {
metadata: {
labels: {},
},
},
})
.mockReturnValue({
jsonData: {
metadata: {
labels: {
subscription: 'sub-123',
resourceGroup: 'rg-prod',
},
},
},
});
const { result, rerender } = renderHook(() => useInfoTab(defaultProject));
expect(result.current.namespaceDetails).toBeNull();
expect(result.current.loading).toBe(false);
expect(result.current.revalidating).toBe(false);
act(() => {
rerender();
});
expect(result.current.namespaceDetails).toEqual(cached);
expect(result.current.loading).toBe(false);
expect(result.current.revalidating).toBe(true);
resolveFetch!(cached);
await waitFor(() => expect(result.current.revalidating).toBe(false));
});

Copilot uses AI. Check for mistakes.
@illume illume requested a review from Copilot April 14, 2026 09:54
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

Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 312 to 322
@@ -262,6 +318,7 @@ export const useInfoTab = (project: {
clusterName,
resourceGroup,
namespaceName: projectId,
subscriptionId: subscription,
ingressPolicy: formData.ingress,
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

handleSave does not guard against a missing subscription, but passes subscriptionId: subscription to updateManagedNamespace. If subscription is undefined (e.g., labels still resolving), this can trigger an invalid CLI call. Consider including subscription (or cacheKey) in the early-return guard so the update cannot run without a fully-qualified target.

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +145
const cacheKey =
clusterName && projectId && resourceGroup && subscription
? `${subscription}/${resourceGroup}/${clusterName}/${projectId}`
: null;
const cached = cacheKey ? detailsCache.get(cacheKey) ?? null : null;

const [loading, setLoading] = useState(cacheKey !== null && cached === null);
const [revalidating, setRevalidating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [namespaceDetails, setNamespaceDetails] = useState<NamespaceDetails | null>(null);
const [namespaceDetails, setNamespaceDetails] = useState<NamespaceDetails | null>(cached);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The initial state only hydrates from cache on the first render. If subscription/resourceGroup arrive asynchronously (via namespaceInstance), cacheKey is initially null, so cached details for that project won’t be shown immediately once labels resolve; the hook will instead do a cold fetch and show loading. To preserve the intended “instant subsequent opens”, consider adding a small effect that, when cacheKey becomes available and namespaceDetails is still null, re-checks detailsCache and hydrates state before starting a blocking load.

Copilot uses AI. Check for mistakes.
isMounted = false;
};
}, [clusterName, projectId, subscription, resourceGroup]);
}, [clusterName, projectId, subscription, resourceGroup, refreshTick]);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This effect uses t(...) from useTranslation() but t is not included in the dependency array. This can cause stale translations if the locale changes while the component is mounted and may violate react-hooks/exhaustive-deps if enabled. Consider adding t (and optionally cacheKey for readability) to the dependency list.

Suggested change
}, [clusterName, projectId, subscription, resourceGroup, refreshTick]);
}, [cacheKey, clusterName, projectId, resourceGroup, refreshTick, subscription, t]);

Copilot uses AI. Check for mistakes.
"Failed to fetch managed namespaces": "未能提取托管命名空间",
"Failed to fetch managed namespace details": "未能提取托管命名空间详细信息",
"Failed to update managed namespace": "未能更新托管命名空间",
"Refreshing data in background": "",
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Several locales add new keys with empty-string values (e.g., this one). i18n frameworks commonly treat an empty string as a valid translation, which results in blank UI (here: an empty tooltip). Prefer omitting the key to allow fallback to en, or provide an actual translation value.

Suggested change
"Refreshing data in background": "",

Copilot uses AI. Check for mistakes.
Comment on lines +625 to +628
"Failed to fetch metrics from Prometheus": "Failed to fetch metrics from Prometheus",
"Metrics": "Metrics",
"Loading metrics from Prometheus": "Loading metrics from Prometheus",
"Metrics refreshed every 30 seconds": "Metrics refreshed every 30 seconds",
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The PR description is focused on InfoTab caching/refresh, but this PR also adds/reshuffles multiple metrics/Prometheus translation keys across many locales. If these strings are unrelated to the InfoTab change, consider splitting them into a separate PR to keep scope tight and reduce review/merge conflicts in locale files.

Copilot uses AI. Check for mistakes.
@skoeva skoeva marked this pull request as draft April 14, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Info Tab: Doesn't load, infinite loop

4 participants