Skip to content

Commit 975e40c

Browse files
authored
feat: Show favorites first in the attribute sidebar (#625)
* Show favorites first in the attribute sidebar * Fix test
1 parent 7ee8231 commit 975e40c

2 files changed

Lines changed: 147 additions & 1 deletion

File tree

src/components/Explore/AttributesSidebar.test.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,126 @@ describe('AttributesSidebar', () => {
866866
});
867867
});
868868

869+
describe('Favorites First in Scope Tabs', () => {
870+
it('should show favorites first in Resource tab', () => {
871+
mockUseFavoriteAttributes.mockReturnValue({
872+
favoriteAttributes: ['resource.namespace', 'resource.cluster'], // These are favorites
873+
toggleFavorite: jest.fn(),
874+
reorderFavorites: jest.fn(),
875+
});
876+
877+
const { container } = render(
878+
<AttributesSidebar
879+
options={sampleOptions}
880+
selected={undefined}
881+
onAttributeChange={mockOnAttributeChange}
882+
model={mockModel}
883+
showFavorites={true}
884+
/>
885+
);
886+
887+
fireEvent.click(screen.getByText('Resource'));
888+
889+
const items = container.querySelectorAll('li[title]');
890+
const labels = Array.from(items).map((item) => item.textContent);
891+
892+
// Favorites should be first (namespace, cluster), then non-favorites alphabetically (service.name)
893+
expect(labels[0]).toContain('namespace');
894+
expect(labels[1]).toContain('cluster');
895+
expect(labels[2]).toContain('service.name');
896+
});
897+
898+
it('should show favorites first in Span tab', () => {
899+
mockUseFavoriteAttributes.mockReturnValue({
900+
favoriteAttributes: ['span.db.system', 'span.http.method'], // These are favorites
901+
toggleFavorite: jest.fn(),
902+
reorderFavorites: jest.fn(),
903+
});
904+
905+
const { container } = render(
906+
<AttributesSidebar
907+
options={sampleOptions}
908+
selected={undefined}
909+
onAttributeChange={mockOnAttributeChange}
910+
model={mockModel}
911+
showFavorites={true}
912+
/>
913+
);
914+
915+
fireEvent.click(screen.getByText('Span'));
916+
917+
const items = container.querySelectorAll('li[title]');
918+
const labels = Array.from(items).map((item) => item.textContent);
919+
920+
// Favorites should be first in custom order (db.system, http.method), then non-favorites (http.status_code)
921+
expect(labels[0]).toContain('db.system');
922+
expect(labels[1]).toContain('http.method');
923+
expect(labels[2]).toContain('http.status_code');
924+
});
925+
926+
it('should NOT show favorites first in All tab', () => {
927+
mockUseFavoriteAttributes.mockReturnValue({
928+
favoriteAttributes: ['span.db.system', 'resource.namespace'],
929+
toggleFavorite: jest.fn(),
930+
reorderFavorites: jest.fn(),
931+
});
932+
933+
const { container } = render(
934+
<AttributesSidebar
935+
options={sampleOptions}
936+
selected={undefined}
937+
onAttributeChange={mockOnAttributeChange}
938+
model={mockModel}
939+
showFavorites={true}
940+
/>
941+
);
942+
943+
fireEvent.click(screen.getByText('All'));
944+
945+
const items = container.querySelectorAll('li[title]');
946+
const labels = Array.from(items).map((item) => item.textContent);
947+
948+
// Should be in alphabetical order, not favorites first
949+
// Alphabetical: cluster, db.system, http.method, http.status_code, namespace, service.name
950+
expect(labels[0]).toContain('cluster');
951+
expect(labels[1]).toContain('db.system');
952+
expect(labels[2]).toContain('http.method');
953+
});
954+
955+
it('should apply search filter to favorites-first ordering in Resource tab', () => {
956+
mockUseFavoriteAttributes.mockReturnValue({
957+
favoriteAttributes: ['resource.cluster', 'resource.namespace'],
958+
toggleFavorite: jest.fn(),
959+
reorderFavorites: jest.fn(),
960+
});
961+
962+
const { container } = render(
963+
<AttributesSidebar
964+
options={sampleOptions}
965+
selected={undefined}
966+
onAttributeChange={mockOnAttributeChange}
967+
model={mockModel}
968+
showFavorites={true}
969+
/>
970+
);
971+
972+
fireEvent.click(screen.getByText('Resource'));
973+
974+
// Search for 'c'
975+
const searchInput = screen.getByPlaceholderText('Search attributes...');
976+
fireEvent.change(searchInput, { target: { value: 'c' } });
977+
978+
const items = container.querySelectorAll('li[title]');
979+
const labels = Array.from(items).map((item) => item.textContent);
980+
981+
// Should show favorites first (both contain 'c'), then non-favorites
982+
// 'cluster' contains 'c', 'namespace' contains 'c' at the end, both are favorites
983+
expect(labels[0]).toContain('cluster');
984+
expect(labels[1]).toContain('namespace');
985+
expect(labels[2]).toContain('service.name'); // non-favorite that contains 'c'
986+
});
987+
});
988+
869989
describe('Edge Cases', () => {
870990
it('should handle empty options array', () => {
871991
render(

src/components/Explore/AttributesSidebar.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function AttributesSidebar({
137137
return favoritesItems.filter((item) => matchesSearchValue(item.label, searchValue));
138138
}
139139

140-
return attributeItems.filter((item) => {
140+
const matchingItems = attributeItems.filter((item) => {
141141
// Filter by search text
142142
const matchesSearch = matchesSearchValue(item.label, searchValue);
143143

@@ -146,6 +146,32 @@ export function AttributesSidebar({
146146

147147
return matchesSearch && matchesScope;
148148
});
149+
150+
// For Resource and Span tabs, show favorites first
151+
if (selectedScope === 'Resource' || selectedScope === 'Span') {
152+
const favorites: AttributeItem[] = [];
153+
const nonFavorites: AttributeItem[] = [];
154+
155+
matchingItems.forEach((item) => {
156+
if (favoriteAttributes.includes(item.value)) {
157+
favorites.push(item);
158+
} else {
159+
nonFavorites.push(item);
160+
}
161+
});
162+
163+
favorites.sort((a, b) => {
164+
const indexA = favoriteAttributes.indexOf(a.value);
165+
const indexB = favoriteAttributes.indexOf(b.value);
166+
return indexA - indexB;
167+
});
168+
169+
// Non-favorites are already sorted alphabetically from attributeItems
170+
return [...favorites, ...nonFavorites];
171+
}
172+
173+
// For "All" tab, return items in their original alphabetical order
174+
return matchingItems;
149175
}, [attributeItems, searchValue, selectedScope, favoriteAttributes, matchesSearchValue]);
150176

151177
// Select the next favorite attribute if the selected attribute is in the filters (single mode only)

0 commit comments

Comments
 (0)