diff --git a/public/app/features/stars/StarToolbarButton.test.tsx b/public/app/features/stars/StarToolbarButton.test.tsx
index 2c3e2e7468d..d4fde427014 100644
--- a/public/app/features/stars/StarToolbarButton.test.tsx
+++ b/public/app/features/stars/StarToolbarButton.test.tsx
@@ -1,6 +1,7 @@
-import { render, screen, testWithFeatureToggles } from 'test/test-utils';
+import { render, screen, testWithFeatureToggles, waitFor } from 'test/test-utils';
import { GrafanaConfig, locationUtil } from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
import { config, setBackendSrv } from '@grafana/runtime';
import { setupMockServer } from '@grafana/test-utils/server';
import { getFolderFixtures } from '@grafana/test-utils/unstable';
@@ -110,5 +111,43 @@ describe('StarToolbarButton', () => {
expect(screen.queryByTestId(expectedTestId)).not.toBeInTheDocument();
});
+
+ it('shows spinner initially and transitions to empty star when loading completes with no starred items', async () => {
+ setup(itemToStar);
+ const button = screen.getByTestId(selectors.components.NavToolbar.markAsFavorite);
+
+ // Initially, button should be disabled (loading state)
+ expect(button).toBeDisabled();
+
+ // Wait for loading to complete
+ await waitFor(
+ () => {
+ expect(button).not.toBeDisabled();
+ },
+ { timeout: 3000 }
+ );
+
+ // After loading, should show "Mark as favorite" (empty star state)
+ expect(await findStarButton(itemToStar.title, false)).toBeInTheDocument();
+ });
+
+ it('shows spinner initially and transitions to filled star when loading completes with starred item', async () => {
+ setup(existingStarredItem);
+ const button = screen.getByTestId(selectors.components.NavToolbar.markAsFavorite);
+
+ // Initially, button should be disabled (loading state)
+ expect(button).toBeDisabled();
+
+ // Wait for loading to complete
+ await waitFor(
+ () => {
+ expect(button).not.toBeDisabled();
+ },
+ { timeout: 3000 }
+ );
+
+ // After loading, should show "Unmark as favorite" (filled star state)
+ expect(await findStarButton(existingStarredItem.title, true)).toBeInTheDocument();
+ });
});
});
diff --git a/public/app/features/stars/StarToolbarButton.tsx b/public/app/features/stars/StarToolbarButton.tsx
index b6af50ce36c..9843f5904d2 100644
--- a/public/app/features/stars/StarToolbarButton.tsx
+++ b/public/app/features/stars/StarToolbarButton.tsx
@@ -39,7 +39,7 @@ export function StarToolbarButton({ title, group, kind, id, onStarChange }: Prop
onStarChange?.(id, !isStarred);
};
- const iconProps = (() => {
+ const iconProps = useMemo(() => {
if (isLoading) {
return { name: 'spinner', type: 'default' } as const;
}
@@ -47,7 +47,7 @@ export function StarToolbarButton({ title, group, kind, id, onStarChange }: Prop
return { name: 'favorite', type: 'mono' } as const;
}
return { name: 'star', type: 'default' } as const;
- })();
+ }, [isLoading, isStarred]);
const tooltipAndLabel = (() => {
return isStarred
@@ -55,7 +55,7 @@ export function StarToolbarButton({ title, group, kind, id, onStarChange }: Prop
: { tooltip: tooltips.star, label: isLoading ? undefined : tooltips.starWithTitle };
})();
- const icon = ;
+ const icon = ;
return (
{
const name = `user-${contextSrv.user.uid}`;
const appPlatform = config.featureToggles.starsFromAPIServer;
const legacyResponse = useLegacyGetStarsQuery(appPlatform ? skipToken : undefined);
- const appPlatformResponse = useListStarsQuery(!appPlatform ? skipToken : { fieldSelector: `metadata.name=${name}` });
+ const queryArgs = !appPlatform ? skipToken : { fieldSelector: `metadata.name=${name}` };
+ const appPlatformResponse = useListStarsQuery(queryArgs);
const appPlatformStarredItems = useMemo(() => {
- const { data } = appPlatformResponse;
- if (data) {
- const starredItems = appPlatformResponse.data?.items || [];
- if (!starredItems.length) {
- return [];
- }
- return starredItems[0]?.spec.resource.find((info) => info.group === group && info.kind === kind)?.names || [];
+ const { data, isLoading, isUninitialized } = appPlatformResponse;
+
+ // If query hasn't been initiated yet or is still loading, return undefined to show loading state
+ if (isUninitialized || isLoading) {
+ return undefined;
}
- return undefined;
+
+ // If query completed but no data, return empty array
+ if (!data) {
+ return [];
+ }
+
+ const starredItems = appPlatformResponse.data?.items || [];
+ if (!starredItems.length) {
+ return [];
+ }
+
+ return starredItems[0]?.spec.resource.find((info) => info.group === group && info.kind === kind)?.names || [];
}, [appPlatformResponse, group, kind]);
- return appPlatform
- ? {
- ...appPlatformResponse,
- data: appPlatformStarredItems,
- }
- : legacyResponse;
+ if (appPlatform) {
+ return {
+ ...appPlatformResponse,
+ data: appPlatformStarredItems,
+ // Ensure isLoading is true when data is undefined (still loading or uninitialized)
+ isLoading: appPlatformStarredItems === undefined ? true : appPlatformResponse.isLoading,
+ };
+ }
+
+ // For legacy response, ensure isLoading is true when query is uninitialized
+ // RTK Query sets isLoading: false when uninitialized, but we need it to be true
+ return {
+ ...legacyResponse,
+ isLoading: legacyResponse.isUninitialized || legacyResponse.isLoading,
+ };
};
/**