Skip to content

Commit 1e3832d

Browse files
authored
fix/hardware revision selector optional (#1941)
* fix: simplify hardware selectors query Query builds directly instead of joining tests, look back 30 days, and drop custom cache timeout on selectors endpoint. Part of #1939 Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi> * i18n: add hardware listing selector labels Add placeholder, search, empty-state, and clear-selection strings for the optional tree/branch/revision selectors. Part of #1939 Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi> * fix: make hardware revision selector optional Keep both revision-scoped and default hardware listing endpoints, add clearable tree/branch/revision selectors, and include e2e tests. Closes #1939 Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi> --------- Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
1 parent 708da74 commit 1e3832d

9 files changed

Lines changed: 396 additions & 184 deletions

File tree

backend/kernelCI_app/queries/hardware.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,13 @@ def get_hardware_selectors(origin: str) -> list[dict]:
135135
c.start_time
136136
FROM checkouts c
137137
INNER JOIN builds b ON b.checkout_id = c.id
138-
INNER JOIN tests t ON t.build_id = b.id
139138
WHERE
140-
t.origin = %(origin)s
141-
AND t.start_time > (NOW() - INTERVAL '15 days')
142-
AND t.environment_misc ->> 'platform' IS NOT NULL
139+
b.origin = %(origin)s
140+
AND b.start_time > (NOW() - INTERVAL '30 days')
141+
AND c.git_repository_url IS NOT NULL
142+
AND c.git_repository_branch IS NOT NULL
143+
AND c.git_commit_hash IS NOT NULL
144+
AND c.tree_name IS NOT NULL
143145
ORDER BY
144146
c.tree_name,
145147
c.git_repository_url,

backend/kernelCI_app/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def view_cache(view, timeout: int = settings.CACHE_TIMEOUT):
113113
path("log-downloader/", view_cache(views.LogDownloaderView), name="logDownloader"),
114114
path(
115115
"hardware/selectors/",
116-
view_cache(views.HardwareSelectorsView, timeout=settings.CACHE_TIMEOUT * 60),
116+
view_cache(views.HardwareSelectorsView),
117117
name="hardwareSelectors",
118118
),
119119
path(

dashboard/e2e/e2e-selectors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,11 @@ export const COMMON_SELECTORS = {
4242
originDropdown: '[data-test-id="origin-dropdown"]',
4343
originOption: (origin: string) => `[data-test-id="origin-option-${origin}"]`,
4444
} as const;
45+
46+
export const HARDWARE_LISTING_SELECTORS = {
47+
table: 'table',
48+
treeSelector: '[data-test-id="hardware-tree-selector"]',
49+
branchSelector: '[data-test-id="hardware-branch-selector"]',
50+
revisionSelector: '[data-test-id="hardware-revision-selector"]',
51+
clearSelection: '[data-test-id="hardware-selection-clear"]',
52+
} as const;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { test, expect, type Locator, type Page } from '@playwright/test';
2+
3+
import { HARDWARE_LISTING_SELECTORS } from './e2e-selectors';
4+
5+
const SELECTOR_LOAD_TIMEOUT = 15000;
6+
const SELECTION_URL_PARAMS = ['t=', 'gu=', 'gb=', 'ch='] as const;
7+
8+
const getOpenComboboxPopover = (page: Page): Locator =>
9+
page.locator('[role="dialog"][data-state="open"]');
10+
11+
const openComboboxOptions = async (page: Page): Promise<void> => {
12+
const popover = getOpenComboboxPopover(page);
13+
await expect(popover).toBeVisible();
14+
await expect(popover.getByRole('option').first()).toBeVisible();
15+
};
16+
17+
const selectComboboxOption = async (
18+
page: Page,
19+
selector: string,
20+
optionIndex = 0,
21+
): Promise<string> => {
22+
const combobox = page.locator(selector);
23+
await expect(combobox).toBeVisible({ timeout: SELECTOR_LOAD_TIMEOUT });
24+
await expect(combobox).toBeEnabled();
25+
await combobox.click();
26+
await openComboboxOptions(page);
27+
28+
const option = getOpenComboboxPopover(page)
29+
.getByRole('option')
30+
.nth(optionIndex);
31+
const label = (await option.textContent())?.trim() ?? '';
32+
await option.click();
33+
34+
return label;
35+
};
36+
37+
const expectSelectionInUrl = async (page: Page): Promise<void> => {
38+
for (const param of SELECTION_URL_PARAMS) {
39+
await expect(page).toHaveURL(new RegExp(`[?&]${param}`));
40+
}
41+
};
42+
43+
const expectNoSelectionInUrl = (page: Page): void => {
44+
const url = page.url();
45+
for (const param of SELECTION_URL_PARAMS) {
46+
expect(url).not.toMatch(new RegExp(`[?&]${param}`));
47+
}
48+
};
49+
50+
test.describe('Hardware Listing Page Tests', () => {
51+
test.beforeEach(async ({ page }) => {
52+
await page.goto('/hardware');
53+
await expect(
54+
page.locator(HARDWARE_LISTING_SELECTORS.treeSelector),
55+
).toBeVisible({ timeout: SELECTOR_LOAD_TIMEOUT });
56+
});
57+
58+
test('loads hardware listing page correctly', async ({ page }) => {
59+
await expect(page).toHaveTitle(/KernelCI/);
60+
await expect(page).toHaveURL(/\/hardware/);
61+
await expect(page.locator(HARDWARE_LISTING_SELECTORS.table)).toBeVisible();
62+
});
63+
64+
test('selecting a tree auto-fills branch and revision', async ({ page }) => {
65+
const treeLabel = await selectComboboxOption(
66+
page,
67+
HARDWARE_LISTING_SELECTORS.treeSelector,
68+
);
69+
70+
await expectSelectionInUrl(page);
71+
72+
const treeSelector = page.locator(HARDWARE_LISTING_SELECTORS.treeSelector);
73+
const branchSelector = page.locator(
74+
HARDWARE_LISTING_SELECTORS.branchSelector,
75+
);
76+
const revisionSelector = page.locator(
77+
HARDWARE_LISTING_SELECTORS.revisionSelector,
78+
);
79+
80+
await expect(treeSelector).toContainText(treeLabel);
81+
await expect(branchSelector).not.toContainText('Select branch');
82+
await expect(revisionSelector).not.toContainText('Select revision');
83+
await expect(branchSelector).toBeEnabled();
84+
await expect(revisionSelector).toBeEnabled();
85+
});
86+
87+
test('selecting a branch auto-fills revision', async ({ page }) => {
88+
await selectComboboxOption(page, HARDWARE_LISTING_SELECTORS.treeSelector);
89+
await expectSelectionInUrl(page);
90+
91+
const branchBefore = new URL(page.url()).searchParams.get('gb');
92+
const revisionBefore = new URL(page.url()).searchParams.get('ch');
93+
94+
const branchSelector = page.locator(
95+
HARDWARE_LISTING_SELECTORS.branchSelector,
96+
);
97+
await branchSelector.click();
98+
await openComboboxOptions(page);
99+
100+
const options = getOpenComboboxPopover(page).getByRole('option');
101+
const optionCount = await options.count();
102+
test.skip(
103+
optionCount < 2,
104+
'Need at least two branches to verify branch auto-select',
105+
);
106+
107+
await options.nth(1).click();
108+
109+
await expect(page).toHaveURL(/[?&]ch=/);
110+
await expect(
111+
page.locator(HARDWARE_LISTING_SELECTORS.revisionSelector),
112+
).not.toContainText('Select revision');
113+
114+
const branchAfter = new URL(page.url()).searchParams.get('gb');
115+
const revisionAfter = new URL(page.url()).searchParams.get('ch');
116+
expect(branchAfter).toBeTruthy();
117+
expect(branchAfter).not.toBe(branchBefore);
118+
expect(revisionAfter).toBeTruthy();
119+
expect(revisionAfter).not.toBe(revisionBefore);
120+
});
121+
122+
test('clear selection removes selector params from URL', async ({ page }) => {
123+
await selectComboboxOption(page, HARDWARE_LISTING_SELECTORS.treeSelector);
124+
await expectSelectionInUrl(page);
125+
126+
const clearButton = page.locator(HARDWARE_LISTING_SELECTORS.clearSelection);
127+
test.skip(
128+
!(await clearButton.isVisible()),
129+
'Clear selection button is not available in this deployment',
130+
);
131+
132+
await clearButton.click();
133+
134+
expectNoSelectionInUrl(page);
135+
await expect(
136+
page.locator(HARDWARE_LISTING_SELECTORS.treeSelector),
137+
).toContainText('Select tree');
138+
await expect(clearButton).toBeHidden();
139+
});
140+
});

dashboard/src/api/hardware.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const useHardwareListing = (
5353
endTimestampInSeconds: number,
5454
searchFrom: HardwareListingRoutesMap['search'],
5555
commitsList?: string[],
56+
enabled = true,
5657
): UseQueryResult<HardwareListingResponse> => {
5758
const { origin } = useSearch({ from: searchFrom });
5859

@@ -73,6 +74,7 @@ export const useHardwareListing = (
7374
endTimestampInSeconds,
7475
commitsList,
7576
),
77+
enabled,
7678
refetchOnWindowFocus: false,
7779
});
7880
};

dashboard/src/locales/messages/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,19 +217,29 @@ export const messages = {
217217
'hardwareDetails.timeFrame':
218218
'Results from {startDate} and {startTime} to {endDate} {endTime}',
219219
'hardwareListing.bannerTitle': 'Hardware Listing',
220+
'hardwareListing.branchSelectorEmpty': 'No branch found.',
220221
'hardwareListing.branchSelectorLabel': 'Branch',
222+
'hardwareListing.branchSelectorPlaceholder': 'Select branch',
223+
'hardwareListing.branchSelectorSearchPlaceholder': 'Search branch...',
224+
'hardwareListing.clearSelection': 'Clear selection',
221225
'hardwareListing.description': 'List of hardware from kernel tests',
222226
'hardwareListing.notFound': 'No hardware information available',
223227
'hardwareListing.revisionEmpty':
224228
'The selected revision has no hardware rows yet. Data ingestion may still be in progress.',
229+
'hardwareListing.revisionSelectorEmpty': 'No revision found.',
225230
'hardwareListing.revisionSelectorLabel': 'Revision',
231+
'hardwareListing.revisionSelectorPlaceholder': 'Select revision',
232+
'hardwareListing.revisionSelectorSearchPlaceholder': 'Search revision...',
226233
'hardwareListing.selectionResetDescription':
227234
'The previous tree/branch/revision selection has no qualifying data for the selected origin and was reset to the latest available revision.',
228235
'hardwareListing.selectionResetTitle': 'Hardware selection reset',
229236
'hardwareListing.selectorsNoData':
230237
'No qualifying hardware data is available for the selected origin yet.',
231238
'hardwareListing.title': 'Hardware Listing ― KCI Dashboard',
239+
'hardwareListing.treeSelectorEmpty': 'No tree found.',
232240
'hardwareListing.treeSelectorLabel': 'Tree',
241+
'hardwareListing.treeSelectorPlaceholder': 'Select tree',
242+
'hardwareListing.treeSelectorSearchPlaceholder': 'Search tree...',
233243
'issue.alsoPresentTooltip': 'Issue also present in {tree}',
234244
'issue.firstSeen': 'First seen',
235245
'issue.newIssue': 'New issue: This is the first time this issue was seen',

0 commit comments

Comments
 (0)