Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
ce8d551
Add site map CSV import export
flesher Jul 15, 2026
59c0029
Address site map CSV review feedback
flesher Jul 15, 2026
4b81c02
Stream site map CSV chunks within sections
flesher Jul 15, 2026
0373d5a
Address site map security review
flesher Jul 15, 2026
173a637
Handle site map grid and slot edge cases
flesher Jul 15, 2026
365f7ac
Address remaining site map review feedback
flesher Jul 15, 2026
5ef14d0
Fix site map export disambiguation
flesher Jul 15, 2026
6ba7774
Drop site map miner rename gate
flesher Jul 15, 2026
c70defa
Fix mobile site map overflow in fleet e2e
flesher Jul 15, 2026
9185a09
Address site map import review feedback
flesher Jul 15, 2026
baae468
Refine site map CSV headers
flesher Jul 15, 2026
084adaf
Validate site-less building rack capacity
flesher Jul 15, 2026
33b0735
Export site map as agent-ready zip
flesher Jul 15, 2026
60930a7
Validate hidden rack occupants in site map imports
flesher Jul 15, 2026
13541c7
Handle site map commit validation responses
flesher Jul 15, 2026
0d5f0c3
Wrap site map zip test helper errors
flesher Jul 15, 2026
d190791
Support topology creates in site map import
flesher Jul 16, 2026
6208adb
Escape site map section marker values
flesher Jul 16, 2026
ae9a45d
Validate site map building site targets
flesher Jul 16, 2026
d847e29
Support remove omitted site map rows
flesher Jul 16, 2026
2cfdea9
Cascade rack members on placement ID changes
flesher Jul 16, 2026
7a725fd
Stabilize mobile e2e helper interactions
flesher Jul 16, 2026
bc09a32
Allow remove-omitted imports to reuse miner slots
flesher Jul 16, 2026
0f83789
Stabilize mobile settings nav expansion
flesher Jul 16, 2026
f20fa04
Document site map omission semantics
flesher Jul 16, 2026
ac19ccf
Stabilize site map CSV round trips
flesher Jul 16, 2026
2ba7b99
Stabilize mobile settings nav e2e helper
flesher Jul 16, 2026
4fc3214
Stabilize mobile settings subnav e2e clicks
flesher Jul 16, 2026
de5b847
Add stable IDs to site map CSV
flesher Jul 16, 2026
65f9ee4
Address site map review feedback
flesher Jul 16, 2026
d87aa88
Fix sitemap header lint
flesher Jul 16, 2026
bdb4432
Keep sitemap miner renames transactional
flesher Jul 16, 2026
eb28785
Fix ID-qualified sitemap references
flesher Jul 16, 2026
3ea0c7b
Restore sitemap delete guards
flesher Jul 16, 2026
72bf58a
Fix sitemap ID reference regressions
flesher Jul 16, 2026
1312c16
Tighten sitemap retained topology validation
flesher Jul 16, 2026
70020ac
Fix retained sitemap ID validation gaps
flesher Jul 16, 2026
d268875
Lock sitemap placement targets
flesher Jul 16, 2026
e461ba2
Fix sitemap building import races
flesher Jul 17, 2026
9fea964
Fix sitemap ID-qualified validation gaps
flesher Jul 17, 2026
6a7ac4a
Fix sitemap name normalization gaps
flesher Jul 17, 2026
bcb00e3
Revalidate sitemap imports under locks
flesher Jul 17, 2026
ed6d982
Tighten sitemap retained ID validation
flesher Jul 17, 2026
91eac04
Fix sitemap import review edge cases
flesher Jul 17, 2026
465f3ae
Address remaining sitemap import review issues
flesher Jul 17, 2026
9e26d4b
Tighten sitemap import disambiguation
flesher Jul 17, 2026
dcd5dbc
Validate sitemap import final topology names
flesher Jul 17, 2026
fd39a32
Reject transient sitemap identity collisions
flesher Jul 17, 2026
f8e5387
Fix sitemap rack and building validation gaps
flesher Jul 17, 2026
85e8469
Keep omitted sitemap positions occupied in preview
flesher Jul 17, 2026
83d6eed
Restore device store pq import after rebase
flesher Jul 17, 2026
1852edf
Tighten sitemap ID normalization after rebase
flesher Jul 17, 2026
94e71bd
Resolve desired building IDs for sitemap racks
flesher Jul 17, 2026
2962581
Refresh miners after sitemap imports
flesher Jul 18, 2026
6d860ce
Handle ambiguous sitemap buildings
flesher Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 162 additions & 33 deletions client/e2eTests/protoFleet/pages/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@ export class BasePage {
}

async clickClearAllFilters() {
await this.dismissVisibleToastIfPresent();
const clearAllFiltersButton = this.page.getByRole("button", { name: "Clear all filters", exact: true });
await clearAllFiltersButton.scrollIntoViewIfNeeded();
await clearAllFiltersButton.click();
await this.dismissVisibleToastIfPresent();
try {
await clearAllFiltersButton.click();
} catch (error) {
if (!(error instanceof Error) || !error.message.includes("intercepts pointer events")) {
throw error;
}
await this.dismissVisibleToastIfPresent();
await clearAllFiltersButton.click();
}
}

async clearActiveFilter(filterValue: string) {
Expand Down Expand Up @@ -367,22 +375,23 @@ export class BasePage {
}

const logoutButton = this.page.getByTestId("logout-button");

if (!(await logoutButton.isVisible().catch(() => false))) {
await this.clickNavigationMenuIfMobile();
if (await logoutButton.isVisible().catch(() => false)) {
await logoutButton.click();
return;
}

if (this.page.url().includes("/auth") || (await loginForm.isVisible().catch(() => false))) {
await this.clickNavigationMenuIfMobile();
if (await logoutButton.isVisible().catch(() => false)) {
await logoutButton.click();
return;
}

if (!(await logoutButton.isVisible().catch(() => false))) {
await this.page.goto("/auth");
await expect(loginForm).toBeVisible();
if (this.page.url().includes("/auth") || (await loginForm.isVisible().catch(() => false))) {
return;
}

await logoutButton.click();
await this.page.goto("/auth");
await expect(loginForm).toBeVisible();
}

async validateTitle(expectedTitle: string) {
Expand Down Expand Up @@ -500,34 +509,152 @@ export class BasePage {

private async dismissVisibleToastIfPresent() {
const toastContainer = this.page.getByTestId("toaster-container");
if (!(await toastContainer.isVisible().catch(() => false))) {
return;
}
if (await toastContainer.isVisible().catch(() => false)) {
for (let attempt = 0; attempt < 3; attempt += 1) {
await this.dismissToast().catch(() => undefined);
await toastContainer.waitFor({ state: "hidden", timeout: OVERLAY_DISMISS_TIMEOUT }).catch(() => undefined);

for (let attempt = 0; attempt < 3; attempt += 1) {
await this.dismissToast().catch(() => undefined);
await toastContainer.waitFor({ state: "hidden", timeout: OVERLAY_DISMISS_TIMEOUT }).catch(() => undefined);
if (!(await toastContainer.isVisible().catch(() => false))) {
break;
}
}
}

if (!(await toastContainer.isVisible().catch(() => false))) {
return;
const basicToasts = this.page.getByTestId("toast");
const count = await basicToasts.count();
for (let index = count - 1; index >= 0; index -= 1) {
const toast = basicToasts.nth(index);
if (await toast.isVisible().catch(() => false)) {
await toast
.locator("button")
.last()
.evaluate((button: Element) => {
(button as HTMLElement).click();
})
.catch(() => undefined);
}
}

await this.page
.waitForFunction(
() =>
Array.from(document.querySelectorAll('[data-testid="toast"]')).every((toast) => {
const element = toast as HTMLElement;
const style = window.getComputedStyle(element);
return (
element.getClientRects().length === 0 ||
style.display === "none" ||
style.visibility === "hidden" ||
style.opacity === "0"
);
}),
undefined,
{ timeout: OVERLAY_DISMISS_TIMEOUT },
)
.catch(() => undefined);
}

async validateButtonIsVisible(text: string) {
await expect(this.page.getByRole("button", { name: text })).toBeVisible();
}

async clickNavigationMenuIfMobile() {
if (this.isMobile) {
await this.page.getByTestId("navigation-menu-button").click();
if (!this.isMobile) {
return;
}

if (
await this.page
.getByTestId("navigation-menu")
.isVisible()
.catch(() => false)
) {
return;
}

const navigationMenuButton = this.page.getByTestId("navigation-menu-button");
if (await navigationMenuButton.isVisible().catch(() => false)) {
await navigationMenuButton.click();
return;
}

if (
await this.page
.locator(`//input[@id='username']`)
.isVisible()
.catch(() => false)
) {
return;
}

await navigationMenuButton.click();
}

async clickExpandSettingsIfMobile() {
if (this.isMobile && !this.page.url().includes("/settings")) {
await this.page.getByTestId("navigation-menu").getByText("Settings").click();
if (!this.isMobile || this.page.url().includes("/settings")) {
return;
}

const secondaryNav = this.page.getByTestId("secondary-nav");
let lastToggleError: unknown;
for (let attempt = 0; attempt < 5; attempt += 1) {
await this.clickNavigationMenuIfMobile();
const settingsToggle = this.page.getByTestId("navigation-menu").getByRole("button", {
name: "Settings menu toggle",
});
await settingsToggle.waitFor({ state: "visible", timeout: OVERLAY_DISMISS_TIMEOUT }).catch(() => undefined);
await settingsToggle
.evaluate((element) => {
(element as HTMLElement).click();
})
.then(() => {
lastToggleError = undefined;
})
.catch((error: unknown) => {
lastToggleError = error;
});
await secondaryNav.waitFor({ state: "visible", timeout: OVERLAY_DISMISS_TIMEOUT }).catch(() => undefined);
if (await secondaryNav.isVisible().catch(() => false)) {
return;
}
}

if (lastToggleError instanceof Error && lastToggleError.message.includes("Element is not attached to the DOM")) {
throw lastToggleError;
}
await expect(secondaryNav).toBeVisible();
}

private async clickSettingsSubnavLink(path: string) {
const link = this.page.getByTestId("secondary-nav").locator(`a[href="${path}"]`);
if (!this.isMobile) {
await link.click();
return;
}

let lastClickError: unknown;
for (let attempt = 0; attempt < 5; attempt += 1) {
await link.waitFor({ state: "visible", timeout: OVERLAY_DISMISS_TIMEOUT }).catch(() => undefined);
await link
.evaluate((element) => {
(element as HTMLElement).click();
})
.then(() => {
lastClickError = undefined;
})
.catch((error: unknown) => {
lastClickError = error;
});

if (lastClickError === undefined) {
return;
}
}

if (lastClickError instanceof Error) {
throw lastClickError;
}
await link.click();
}

async navigateToHomePage() {
Expand Down Expand Up @@ -605,47 +732,47 @@ export class BasePage {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/security"]').click();
await this.clickSettingsSubnavLink("/settings/security");
await expect(this.page).toHaveURL(/.*\/settings\/security/);
}

async navigateToNetworkSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/network"]').click();
await this.clickSettingsSubnavLink("/settings/network");
await expect(this.page).toHaveURL(/.*\/settings\/network/);
}

async navigateToPreferencesSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/preferences"]').click();
await this.clickSettingsSubnavLink("/settings/preferences");
await expect(this.page).toHaveURL(/.*\/settings\/preferences/);
}

async navigateToTeamSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/team"]').click();
await this.clickSettingsSubnavLink("/settings/team");
await expect(this.page).toHaveURL(/.*\/settings\/team/);
}

async navigateToMiningPoolsSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/mining-pools"]').click();
await this.clickSettingsSubnavLink("/settings/mining-pools");
await expect(this.page).toHaveURL(/.*\/settings\/mining-pools/);
}

async navigateToFirmwareSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/firmware"]').click();
await this.clickSettingsSubnavLink("/settings/firmware");
await expect(this.page).toHaveURL(/.*\/settings\/firmware/);
}

Expand All @@ -661,39 +788,39 @@ export class BasePage {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/integrations"]').click();
await this.clickSettingsSubnavLink("/settings/integrations");
await expect(this.page).toHaveURL(/.*\/settings\/integrations/);
}

async navigateToSchedulesSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/schedules"]').click();
await this.clickSettingsSubnavLink("/settings/schedules");
await expect(this.page).toHaveURL(/.*\/settings\/schedules/);
}

async navigateToCurtailmentSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/curtailment"]').click();
await this.clickSettingsSubnavLink("/settings/curtailment");
await expect(this.page).toHaveURL(/.*\/settings\/curtailment/);
}

async navigateToAlertsSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/alerts"]').click();
await this.clickSettingsSubnavLink("/settings/alerts");
await expect(this.page).toHaveURL(/.*\/settings\/alerts/);
}

async navigateToServerLogsSettings() {
await this.clickNavigationMenuIfMobile();
await this.clickExpandSettingsIfMobile();
await this.navigateSettingsIfDesktop();
await this.page.getByTestId("secondary-nav").locator('a[href="/settings/server-logs"]').click();
await this.clickSettingsSubnavLink("/settings/server-logs");
await expect(this.page).toHaveURL(/.*\/settings\/server-logs/);
}

Expand Down Expand Up @@ -1000,6 +1127,7 @@ export class BasePage {
"building-page-action-sheet-content",
"list-header-action-sheet-content",
"rack-slot-actions-sheet-content",
"site-map-action-sheet-content",
];

await expect
Expand Down Expand Up @@ -1040,6 +1168,7 @@ export class BasePage {
"building-page-action-sheet",
"list-header-action-sheet",
"rack-slot-actions-sheet",
"site-map-action-sheet",
];

for (const testId of sheetTestIds) {
Expand Down
3 changes: 3 additions & 0 deletions client/src/protoFleet/api/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { PairingService } from "@/protoFleet/api/generated/pairing/v1/pairing_pb
import { PoolsService } from "@/protoFleet/api/generated/pools/v1/pools_pb";
import { ScheduleService } from "@/protoFleet/api/generated/schedule/v1/schedule_pb";
import { ServerLogService } from "@/protoFleet/api/generated/serverlog/v1/serverlog_pb";
import { SiteMapService } from "@/protoFleet/api/generated/sitemap/v1/sitemap_pb";
import { SiteService } from "@/protoFleet/api/generated/sites/v1/sites_pb";
import { TelemetryService } from "@/protoFleet/api/generated/telemetry/v1/telemetry_pb";

Expand All @@ -43,6 +44,7 @@ const minerCommandClient = createClient(MinerCommandService, transport);
const poolsClient = createClient(PoolsService, transport);
const scheduleClient = createClient(ScheduleService, transport);
const serverLogClient = createClient(ServerLogService, transport);
const siteMapClient = createClient(SiteMapService, transport);
const deviceSetClient = createClient(DeviceSetService, transport);
const telemetryClient = createClient(TelemetryService, transport);
const foremanImportClient = createClient(ForemanImportService, transport);
Expand Down Expand Up @@ -77,6 +79,7 @@ export {
poolsClient,
scheduleClient,
serverLogClient,
siteMapClient,
sitesClient,
telemetryClient,
foremanImportClient,
Expand Down
Loading