Dashboard Favorites: Dashboard star icon stuck in loading state on initial load (#114211)

Co-Author: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
Nathan Marrs
2025-11-20 11:14:29 +00:00
committed by GitHub
parent 3c777399d5
commit 1d2426f880
3 changed files with 77 additions and 19 deletions
@@ -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();
});
});
});
@@ -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 = <Icon {...iconProps} size="lg" />;
const icon = <Icon {...iconProps} size="lg" key={`${isLoading}-${isStarred}`} />;
return (
<ToolbarButton
disabled={isLoading}
+34 -15
View File
@@ -61,26 +61,45 @@ export const useStarredItems = (group: string, kind: string) => {
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,
};
};
/**