chore: incidents as separate tabs (#9819)

### **PR Type**
Enhancement, Tests


___

### **Description**
- Replace icon tabs with reusable app-tabs component

- Introduce Alerts/Incidents viewMode switching logic

- Add extensive UI tests for new tab functionality

- Update getPath logic and dev script configuration


___

### Diagram Walkthrough


```mermaid
flowchart LR
  TabsComp["app-tabs component"] --> ViewState["viewMode state"]
  ViewState --> AlertsView["Alerts view UI"]
  ViewState --> IncidentsView["Incidents view UI"]
  Tests["UI tests"] --> TabsComp
```



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Bug
fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>zincutils.ts</strong><dd><code>Refine getPath base URL
logic</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

web/src/utils/zincutils.ts

<ul><li>refine getPath logic for web base path<br> <li> conditionally
prepend "/web/" on localhost</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-d3d59688cccdcc6c84ebe88b698c6910030abecc3125f4c0ef81b0df023699ba">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>AlertList.vue</strong><dd><code>Replace custom tabs
with app-tabs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

web/src/components/alerts/AlertList.vue

<ul><li>remove custom icon-only view tabs<br> <li> integrate
<code>app-tabs</code> for view switching<br> <li> add
<code>viewTabs</code> computed property<br> <li> update related SCSS
styling</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-be3e41b823e163b226ffc270d95cc4981e59c600851e4e0d10ee5e4ee994ac8e">+35/-92</a>&nbsp;
</td>

</tr>
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>alertsPage.js</strong><dd><code>Add view mode tab
methods and locators</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

tests/ui-testing/pages/alertsPages/alertsPage.js

<ul><li>add locators for alert and incident tabs<br> <li> implement
click and expect methods for tabs<br> <li> add search methods for both
views</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-79ee01d6d7f145f19207b93b87143b545a1188cfc6a82bc8c45fecfbf68f8a1d">+113/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>alertsIncidentsTabs.spec.js</strong><dd><code>Add E2E
tests for alerts/incidents tabs</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

tests/ui-testing/playwright-tests/Alerts/alertsIncidentsTabs.spec.js

<ul><li>add E2E tests for tabs visibility and switching<br> <li> cover
functional and edge-case scenarios</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-3776665660a181de2d08e4d3bccc481191e7ac88f4e4b0943c869ffa4eb12ffd">+347/-0</a>&nbsp;
</td>

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Add base path to dev
script</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

web/package.json

- update `dev` script to include base `/web/`


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-b861012a5dd72b8a9f3281b7cf09f5a779c98569d040b1bbc1db50f1b15e7cce">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

</details>

___
This commit is contained in:
Ashish Kolhe
2026-01-02 14:11:31 +05:30
committed by GitHub
parent c77bd0b79a
commit cb178787f2
7 changed files with 541 additions and 106 deletions
@@ -142,6 +142,18 @@ export class AlertsPage {
searchAcrossFoldersToggle: '[data-test="alert-list-search-across-folders-toggle"]',
alertDeleteOption: 'Delete',
// View Mode Tabs (Alerts / Incidents) - VERIFIED from source code
alertIncidentViewTabs: '[data-test="alert-incident-view-tabs"]',
alertsViewTab: '[data-test="tab-alerts"]',
incidentsViewTab: '[data-test="tab-incidents"]',
// Incidents view locators
incidentListTable: '[data-test="incident-list-table"]',
incidentSearchInput: '[data-test="incident-search-input"]',
// Import button
alertImportButton: '[data-test="alert-import"]',
// Table locators
tableBodyRowWithIndex: 'tbody tr[data-index]',
tableLocator: 'table',
@@ -630,6 +642,107 @@ export class AlertsPage {
await expect(this.page.locator('[data-test="dashboard-folder-tab-default"]').getByText('default')).toBeVisible();
}
// ==================== VIEW MODE TABS (ALERTS / INCIDENTS) ====================
/**
* Click the Alerts view tab
*/
async clickAlertsTab() {
testLogger.info('Clicking Alerts tab');
await this.page.locator(this.locators.alertsViewTab).click();
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
testLogger.info('Switched to Alerts view');
}
/**
* Click the Incidents view tab
*/
async clickIncidentsTab() {
testLogger.info('Clicking Incidents tab');
await this.page.locator(this.locators.incidentsViewTab).click();
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
testLogger.info('Switched to Incidents view');
}
/**
* Verify the view mode tabs are visible on page load
*/
async expectViewModeTabsVisible() {
testLogger.info('Verifying view mode tabs are visible');
await expect(this.page.locator(this.locators.alertIncidentViewTabs)).toBeVisible({ timeout: 10000 });
await expect(this.page.locator(this.locators.alertsViewTab)).toBeVisible();
await expect(this.page.locator(this.locators.incidentsViewTab)).toBeVisible();
testLogger.info('View mode tabs verified');
}
/**
* Verify Alerts view UI elements are visible
*/
async expectAlertsViewElementsVisible() {
testLogger.info('Verifying Alerts view elements');
await expect(this.page.locator('[data-test="alert-list-table"]')).toBeVisible({ timeout: 10000 });
await expect(this.page.locator(this.locators.alertSearchInput)).toBeVisible();
await expect(this.page.locator(this.locators.searchAcrossFoldersToggle)).toBeVisible();
await expect(this.page.locator(this.locators.alertImportButton)).toBeVisible();
await expect(this.page.locator(this.locators.addAlertButton)).toBeVisible();
testLogger.info('Alerts view elements verified');
}
/**
* Verify Incidents view UI elements are visible
*/
async expectIncidentsViewElementsVisible() {
testLogger.info('Verifying Incidents view elements');
await expect(this.page.locator(this.locators.incidentListTable)).toBeVisible({ timeout: 10000 });
await expect(this.page.locator(this.locators.incidentSearchInput)).toBeVisible();
testLogger.info('Incidents view elements verified');
}
/**
* Verify Alerts-only elements are hidden in Incidents view
*/
async expectAlertsOnlyElementsHidden() {
testLogger.info('Verifying Alerts-only elements are hidden');
await expect(this.page.locator('[data-test="alert-list-table"]')).not.toBeVisible();
await expect(this.page.locator(this.locators.searchAcrossFoldersToggle)).not.toBeVisible();
await expect(this.page.locator(this.locators.alertImportButton)).not.toBeVisible();
await expect(this.page.locator(this.locators.addAlertButton)).not.toBeVisible();
testLogger.info('Alerts-only elements confirmed hidden');
}
/**
* Verify Incidents-only elements are hidden in Alerts view
*/
async expectIncidentsOnlyElementsHidden() {
testLogger.info('Verifying Incidents-only elements are hidden');
await expect(this.page.locator(this.locators.incidentListTable)).not.toBeVisible();
await expect(this.page.locator(this.locators.incidentSearchInput)).not.toBeVisible();
testLogger.info('Incidents-only elements confirmed hidden');
}
/**
* Search in Alerts view
*/
async searchInAlertsView(query) {
testLogger.info(`Searching in Alerts view: ${query}`);
const searchInput = this.page.locator(this.locators.alertSearchInput);
await searchInput.click();
await searchInput.clear();
await searchInput.type(query);
await this.page.waitForTimeout(500); // Wait for debounce
testLogger.info('Search applied in Alerts view');
}
/**
* Search in Incidents view
*/
async searchInIncidentsView(query) {
testLogger.info(`Searching in Incidents view: ${query}`);
await this.page.locator(this.locators.incidentSearchInput).fill(query);
await this.page.waitForTimeout(500); // Wait for debounce
testLogger.info('Search applied in Incidents view');
}
// ==================== IMPORT/EXPORT OPERATIONS ====================
async exportAlerts() {
+2 -1
View File
@@ -25,7 +25,8 @@ export class CommonActions {
async navigateToAlerts() {
await this.page.locator(this.alertsMenuItem).click();
await this.page.waitForTimeout(2000);
await expect(this.page.locator('[data-test="alerts-list-title"]')).toContainText('Alerts');
// Updated to check for view mode tabs instead of old title element
await expect(this.page.locator('[data-test="alert-incident-view-tabs"]')).toBeVisible();
}
async navigateToSettings() {
@@ -0,0 +1,347 @@
/**
* Alerts & Incidents Tab Separation - E2E Tests
*
* Test Suite: Validates the new tab-based UI for separating Alerts and Incidents views
* Module: Alerts
* Priority: High (Core UX Enhancement)
*
* Feature: Users can switch between Alerts (configuration) and Incidents (events) using
* distinct tabs with proper conditional rendering of view-specific UI elements.
*
* Test Coverage:
* - P0 (Smoke): Tabs visible, default view, tab switching
* - P1 (Functional): View-specific elements, search functionality
* - P2 (Edge Cases): Empty states, page refresh
*/
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const testLogger = require('../utils/test-logger.js');
const PageManager = require('../../pages/page-manager.js');
const logData = require('../../fixtures/log.json');
test.describe("Alerts & Incidents Tab Separation", () => {
test.describe.configure({ mode: 'parallel' });
let pm;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
// Navigate to alerts page
await page.goto(`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`);
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
testLogger.info('Navigated to homepage');
// Wait for any loading overlays to disappear (wait up to 30 seconds)
const loadingOverlay = page.locator('.fullscreen.bg-blue');
if (await loadingOverlay.isVisible().catch(() => false)) {
await loadingOverlay.waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
}
// Additional wait to ensure page is ready
await page.waitForTimeout(2000);
// Click alerts menu item
await page.locator(pm.alertsPage.locators.alertMenuItem).click();
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
testLogger.info('Navigated to alerts page');
});
// ========================================
// P0 - SMOKE TESTS (Critical Path)
// ========================================
test("TC-01: Tabs visible on page load", {
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
}, async ({ page }) => {
testLogger.info('[P0] Testing tabs visibility on page load');
// Verify tabs container and individual tabs are visible
await pm.alertsPage.expectViewModeTabsVisible();
testLogger.info('[P0] ✓ Tabs verified visible on page load');
});
test("TC-02: Default view is Alerts", {
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
}, async ({ page }) => {
testLogger.info('[P0] Testing default view is Alerts');
// Verify Alerts view UI elements are visible
await pm.alertsPage.expectAlertsViewElementsVisible();
// Verify Incidents-only elements are hidden
await pm.alertsPage.expectIncidentsOnlyElementsHidden();
testLogger.info('[P0] ✓ Default view confirmed as Alerts');
});
test("TC-03: Tab switching works (Alerts → Incidents)", {
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
}, async ({ page }) => {
testLogger.info('[P0] Testing tab switch from Alerts to Incidents');
// Click Incidents tab
await pm.alertsPage.clickIncidentsTab();
// Verify Incidents view elements are visible
await pm.alertsPage.expectIncidentsViewElementsVisible();
// Verify Alerts-only elements are hidden
await pm.alertsPage.expectAlertsOnlyElementsHidden();
testLogger.info('[P0] ✓ Tab switch to Incidents successful');
});
test("TC-04: Tab switching works (Incidents → Alerts)", {
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
}, async ({ page }) => {
testLogger.info('[P0] Testing tab switch from Incidents back to Alerts');
// First switch to Incidents
await pm.alertsPage.clickIncidentsTab();
testLogger.info('Switched to Incidents view');
// Then switch back to Alerts
await pm.alertsPage.clickAlertsTab();
// Verify Alerts view elements are visible again
await pm.alertsPage.expectAlertsViewElementsVisible();
// Verify Incidents-only elements are hidden again
await pm.alertsPage.expectIncidentsOnlyElementsHidden();
testLogger.info('[P0] ✓ Tab switch back to Alerts successful');
});
// ========================================
// P1 - FUNCTIONAL TESTS
// ========================================
test("TC-05: Alerts view shows all required UI elements", {
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
}, async ({ page }) => {
testLogger.info('[P1] Testing Alerts view UI elements');
// Verify all Alerts view elements
await expect(page.locator('[data-test="alert-list-table"]')).toBeVisible({ timeout: 10000 });
await expect(page.locator(pm.alertsPage.locators.alertSearchInput)).toBeVisible();
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).toBeVisible();
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).toBeVisible();
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
// Verify folders panel (splitter) is visible
await expect(page.locator('[data-test="alert-list-splitter"]')).toBeVisible();
testLogger.info('[P1] ✓ All Alerts view UI elements verified');
});
test("TC-06: Incidents view shows only incident UI elements", {
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
}, async ({ page }) => {
testLogger.info('[P1] Testing Incidents view UI elements');
// Switch to Incidents view
await pm.alertsPage.clickIncidentsTab();
// Verify Incidents view elements visible
await pm.alertsPage.expectIncidentsViewElementsVisible();
// Verify Alerts-only elements are NOT visible
await expect(page.locator('[data-test="alert-list-table"]')).not.toBeVisible();
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).not.toBeVisible();
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).not.toBeVisible();
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).not.toBeVisible();
testLogger.info('[P1] ✓ Incidents view UI elements verified');
});
test("TC-07: Search works in Alerts view", {
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all', '@search']
}, async ({ page }) => {
testLogger.info('[P1] Testing search in Alerts view');
// Type in alert search input
const searchQuery = 'test';
const searchInput = page.locator(pm.alertsPage.locators.alertSearchInput);
// Click and focus the input
await searchInput.click();
await page.waitForTimeout(500);
// Type the search query
await searchInput.pressSequentially(searchQuery, { delay: 100 });
await page.waitForTimeout(1000);
// Verify search functionality works (input accepts text and table is visible)
await expect(searchInput).toBeFocused();
await expect(page.locator('[data-test="alert-list-table"]')).toBeVisible();
testLogger.info('[P1] ✓ Search in Alerts view verified');
});
test("TC-08: Search works in Incidents view", {
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all', '@search']
}, async ({ page }) => {
testLogger.info('[P1] Testing search in Incidents view');
// Switch to Incidents view
await pm.alertsPage.clickIncidentsTab();
// Type in incident search input
const searchQuery = 'test';
await pm.alertsPage.searchInIncidentsView(searchQuery);
// Verify search input has the value
await expect(page.locator(pm.alertsPage.locators.incidentSearchInput)).toHaveValue(searchQuery);
// Verify incident table still visible (search results)
await expect(page.locator(pm.alertsPage.locators.incidentListTable)).toBeVisible();
testLogger.info('[P1] ✓ Search in Incidents view verified');
});
test("TC-09: 'All Folders' toggle only in Alerts view", {
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
}, async ({ page }) => {
testLogger.info('[P1] Testing All Folders toggle visibility');
// Verify toggle visible in Alerts view
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).toBeVisible();
testLogger.info('Toggle visible in Alerts view');
// Switch to Incidents view
await pm.alertsPage.clickIncidentsTab();
// Verify toggle hidden in Incidents view
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).not.toBeVisible();
testLogger.info('Toggle hidden in Incidents view');
// Switch back to Alerts view
await pm.alertsPage.clickAlertsTab();
// Verify toggle visible again
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).toBeVisible();
testLogger.info('Toggle visible again in Alerts view');
testLogger.info('[P1] ✓ All Folders toggle visibility verified');
});
test("TC-10: Import and Add Alert buttons only in Alerts view", {
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
}, async ({ page }) => {
testLogger.info('[P1] Testing Import and Add Alert buttons visibility');
// Verify buttons visible in Alerts view
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).toBeVisible();
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
testLogger.info('Buttons visible in Alerts view');
// Switch to Incidents view
await pm.alertsPage.clickIncidentsTab();
// Verify buttons hidden in Incidents view
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).not.toBeVisible();
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).not.toBeVisible();
testLogger.info('Buttons hidden in Incidents view');
// Switch back to Alerts view
await pm.alertsPage.clickAlertsTab();
// Verify buttons visible again
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).toBeVisible();
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
testLogger.info('Buttons visible again in Alerts view');
testLogger.info('[P1] ✓ Import and Add Alert buttons visibility verified');
});
// ========================================
// P2 - EDGE CASE TESTS
// ========================================
test("TC-11: Empty alerts list shows empty state", {
tag: ['@alertsIncidentsTabs', '@edgeCase', '@P2', '@all']
}, async ({ page }) => {
testLogger.info('[P2] Testing empty alerts list handling');
// Even if no alerts, table container should be present
// Note: This test assumes there might be alerts. If no alerts exist,
// the empty state will be visible within the table container.
await expect(page.locator('[data-test="alert-list-table"]')).toBeVisible({ timeout: 10000 });
// Verify tabs still functional
await pm.alertsPage.expectViewModeTabsVisible();
// Verify Add Alert button still visible (key action for empty state)
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
testLogger.info('[P2] ✓ Empty alerts list handled correctly');
});
test("TC-12: Empty incidents list shows empty state", {
tag: ['@alertsIncidentsTabs', '@edgeCase', '@P2', '@all']
}, async ({ page }) => {
testLogger.info('[P2] Testing empty incidents list handling');
// Switch to Incidents view
await pm.alertsPage.clickIncidentsTab();
// Even if no incidents, table container should be present
await expect(page.locator(pm.alertsPage.locators.incidentListTable)).toBeVisible({ timeout: 10000 });
// Verify tabs still functional - can switch back
await pm.alertsPage.clickAlertsTab();
await pm.alertsPage.expectAlertsViewElementsVisible();
testLogger.info('[P2] ✓ Empty incidents list handled correctly');
});
test("TC-13: Page refresh preserves current view", {
tag: ['@alertsIncidentsTabs', '@edgeCase', '@P2', '@all']
}, async ({ page }) => {
testLogger.info('[P2] Testing page refresh preserves view state');
// Get current URL for alerts page
const alertsUrl = page.url();
testLogger.info(`Current alerts URL: ${alertsUrl}`);
// Test 1: Switch to Incidents view
await pm.alertsPage.clickIncidentsTab();
await pm.alertsPage.expectIncidentsViewElementsVisible();
testLogger.info('Switched to Incidents view');
// Get URL with view=incidents parameter
const incidentsUrl = page.url();
testLogger.info(`Incidents URL: ${incidentsUrl}`);
// Refresh page - this should preserve the view parameter
await page.reload({ waitUntil: 'networkidle', timeout: 30000 });
testLogger.info('Page refreshed from Incidents view');
// Wait for any loading overlays
const loadingOverlay = page.locator('.fullscreen.bg-blue');
if (await loadingOverlay.isVisible().catch(() => false)) {
await loadingOverlay.waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
}
// Wait for page to stabilize
await page.waitForTimeout(3000);
// Verify tabs are visible
await pm.alertsPage.expectViewModeTabsVisible();
testLogger.info('Tabs visible after refresh');
// Verify still in Incidents view (URL state preserved)
await pm.alertsPage.expectIncidentsViewElementsVisible();
await pm.alertsPage.expectAlertsOnlyElementsHidden();
testLogger.info('✓ Incidents view preserved after refresh');
// Test 2: Switch back to Alerts and verify
await pm.alertsPage.clickAlertsTab();
await pm.alertsPage.expectAlertsViewElementsVisible();
testLogger.info('[P2] ✓ Page refresh preserves view state correctly');
});
});
+1 -1
View File
@@ -5,7 +5,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --base /web/",
"devcloud": "NODE_ENV=devcloud vite --mode=devcloud",
"build-alpha1": "run-p type-check build-only-alpha1",
"build-cloud-prod": "run-p type-check build-only-cloud-prod",
+42 -11
View File
@@ -134,9 +134,7 @@ async function mountAlertList() {
emits: ["update:active-tab"],
template:
'<div class="app-tabs-stub">' +
'<button class="tab-all" @click="$emit(\'update:active-tab\', \'all\')">All</button>' +
'<button class="tab-scheduled" @click="$emit(\'update:active-tab\', \'scheduled\')">Scheduled</button>' +
'<button class="tab-realtime" @click="$emit(\'update:active-tab\', \'realTime\')">RealTime</button>' +
'<button v-for="tab in tabs" :key="tab.value" :class="`tab-${tab.value}`" @click="$emit(\'update:active-tab\', tab.value)">{{tab.label}}</button>' +
'</div>',
},
SelectFolderDropDown: true,
@@ -295,11 +293,12 @@ const waitData = async (wrapper: any) => {
// 1. Basic rendering and structure
describe("AlertList - basic rendering", () => {
it("renders the page container and title", async () => {
it("renders the page container and view mode tabs", async () => {
const wrapper = await mountAlertList();
await waitData(wrapper);
expect(wrapper.find('[data-test="alert-list-page"]').exists()).toBe(true);
expect(wrapper.get('[data-test="alerts-list-title"]').text()).toBeTruthy();
// Verify view mode tabs exist (Alerts/Incidents tabs)
expect(wrapper.find('[data-test="alert-incident-view-tabs"]').exists()).toBe(true);
});
it("renders search input and toggle", async () => {
@@ -336,20 +335,26 @@ describe("AlertList - data fetching and columns", () => {
const wrapper: any = await mountAlertList();
await waitData(wrapper);
// Ensure we're in alerts view mode first
wrapper.vm.viewMode = 'alerts';
await flushPromises();
// default tab: all
wrapper.vm.activeTab = 'all';
await flushPromises();
let names = wrapper.vm.columns.map((c: any) => c.name);
expect(names).toContain("period");
expect(names).toContain("frequency");
// switch to realTime
await wrapper.find(".tab-realtime").trigger("click");
wrapper.vm.activeTab = 'realTime';
await flushPromises();
names = wrapper.vm.columns.map((c: any) => c.name);
expect(names).not.toContain("period");
expect(names).not.toContain("frequency");
// switch to scheduled
await wrapper.find(".tab-scheduled").trigger("click");
wrapper.vm.activeTab = 'scheduled';
await flushPromises();
names = wrapper.vm.columns.map((c: any) => c.name);
expect(names).toContain("period");
@@ -381,7 +386,13 @@ describe("AlertList - filtering behaviors", () => {
const wrapper: any = await mountAlertList();
await waitData(wrapper);
await wrapper.find(".tab-scheduled").trigger("click");
// Ensure we're in alerts view mode
wrapper.vm.viewMode = 'alerts';
await flushPromises();
// Set activeTab and trigger filtering
wrapper.vm.activeTab = 'scheduled';
wrapper.vm.filterAlertsByTab(true);
await flushPromises();
expect(wrapper.vm.filteredResults.every((r: any) => !r.is_real_time)).toBe(true);
@@ -391,7 +402,13 @@ describe("AlertList - filtering behaviors", () => {
const wrapper: any = await mountAlertList();
await waitData(wrapper);
await wrapper.find(".tab-realtime").trigger("click");
// Ensure we're in alerts view mode
wrapper.vm.viewMode = 'alerts';
await flushPromises();
// Set activeTab and trigger filtering
wrapper.vm.activeTab = 'realTime';
wrapper.vm.filterAlertsByTab(true);
await flushPromises();
expect(wrapper.vm.filteredResults.every((r: any) => r.is_real_time)).toBe(true);
@@ -415,7 +432,13 @@ describe("AlertList - filtering behaviors", () => {
const wrapper: any = await mountAlertList();
await waitData(wrapper);
await wrapper.find(".tab-scheduled").trigger("click");
// Ensure we're in alerts view mode
wrapper.vm.viewMode = 'alerts';
await flushPromises();
// Set activeTab and trigger filtering
wrapper.vm.activeTab = 'scheduled';
wrapper.vm.filterAlertsByTab(true);
await flushPromises();
wrapper.vm.filterAlertsByQuery("Scheduled");
@@ -662,8 +685,16 @@ describe("AlertList - additional validations", () => {
it(`tab ${tab} shows only matching rows (${idx})`, async () => {
const wrapper: any = await mountAlertList();
await waitData(wrapper);
await wrapper.find(`.tab-${tab === "all" ? "all" : tab.toLowerCase()}`).trigger("click");
// Ensure we're in alerts view mode
wrapper.vm.viewMode = 'alerts';
await flushPromises();
// Set activeTab and trigger filtering
wrapper.vm.activeTab = tab;
wrapper.vm.filterAlertsByTab(true);
await flushPromises();
expect(wrapper.vm.filteredResults.every((r: any) => predicate(r))).toBe(true);
});
});
+35 -92
View File
@@ -27,38 +27,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="flex justify-between full-width tw:py-3 tw:px-4 items-center"
>
<div class="tw:flex tw:items-center tw:gap-4">
<!-- Icon-only View Mode Tabs -->
<div class="view-mode-tabs">
<q-btn
flat
dense
:class="viewMode === 'alerts' ? 'active-tab' : 'inactive-tab'"
class="view-tab-btn"
@click="onViewModeChange('alerts')"
data-test="alerts-view-btn"
>
<q-icon name="notifications_active" size="18px" />
<q-tooltip>{{ t('alerts.header') }}</q-tooltip>
</q-btn>
<q-btn
flat
dense
:class="viewMode === 'incidents' ? 'active-tab' : 'inactive-tab'"
class="view-tab-btn"
@click="onViewModeChange('incidents')"
data-test="incidents-view-btn"
>
<q-icon name="crisis_alert" size="18px" />
<q-tooltip>{{ t('alerts.incidents.title') }}</q-tooltip>
</q-btn>
</div>
<!-- Page Title -->
<div
class="page-title tw:font-[600] tw:text-[20px]"
:data-test="viewMode === 'alerts' ? 'alerts-list-title' : 'incidents-list-title'"
>
{{ viewMode === 'alerts' ? t('alerts.header') : t('alerts.incidents.title') }}
<!-- View Mode Tabs (Alerts / Incidents) -->
<div class="view-mode-tabs-container">
<app-tabs
:tabs="viewTabs"
v-model:active-tab="viewMode"
@update:active-tab="onViewModeChange"
data-test="alert-incident-view-tabs"
/>
</div>
</div>
<div class="flex q-ml-auto tw:ps-2 items-center">
@@ -1174,6 +1150,18 @@ export default defineComponent({
// Incident search query
const incidentSearchQuery = ref("");
// View mode tabs (Alerts / Incidents)
const viewTabs = computed(() => [
{
label: t('alerts.header'),
value: 'alerts',
},
{
label: t('alerts.incidents.title'),
value: 'incidents',
}
]);
// Tabs for alerts view only (removed incidents tab)
const alertTabs = reactive([
{
@@ -2788,6 +2776,7 @@ export default defineComponent({
tabs,
alertTabs,
viewMode,
viewTabs,
onViewModeChange,
incidentSearchQuery,
filterAlertsByTab,
@@ -2812,75 +2801,29 @@ export default defineComponent({
</script>
<style lang="scss">
.view-mode-tabs {
display: flex;
gap: 4px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
padding: 4px;
.view-mode-tabs-container {
margin-right: 24px;
.view-tab-btn {
min-width: 32px;
min-height: 32px;
width: 32px;
height: 32px;
border-radius: 4px;
transition: all 0.2s ease;
&.active-tab {
background: var(--q-primary);
color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
.q-icon {
color: white;
}
// Customize app-tabs for view mode switching
::v-deep .app-tabs {
.q-tabs {
min-height: 36px;
}
&.inactive-tab {
color: rgba(0, 0, 0, 0.54);
.q-tab {
padding: 0 20px;
min-height: 36px;
text-transform: none;
font-weight: 600;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
.q-icon {
color: rgba(0, 0, 0, 0.54);
&__icon {
font-size: 18px;
margin-right: 8px;
}
}
}
}
.page-title {
color: rgba(0, 0, 0, 0.87);
line-height: 1.2;
}
// Dark mode support
.body--dark {
.view-mode-tabs {
border-color: rgba(255, 255, 255, 0.12);
.view-tab-btn {
&.inactive-tab {
color: rgba(255, 255, 255, 0.7);
&:hover {
background: rgba(255, 255, 255, 0.12);
}
.q-icon {
color: rgba(255, 255, 255, 0.7);
}
}
}
}
.page-title {
color: rgba(255, 255, 255, 0.87);
}
}
.bottom-btn {
display: flex;
width: 100%;
+1 -1
View File
@@ -367,7 +367,7 @@ export const getPath = () => {
const pos = window.location.pathname.indexOf("/web/");
const path =
window.location.origin == "http://localhost:8081"
? "/"
? pos > -1 ? "/web/" : "/"
: pos > -1
? window.location.pathname.slice(0, pos + 5)
: "";