mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
fix: incident list as seperate menu (#10080)
### **PR Type** Enhancement ___ ### **Description** - Extract incidents into standalone page - Remove incidents tab from alerts list - Update router and menu navigation links - Simplify search & layout in IncidentList.vue - Add multilingual translations for incidents ___ ### Diagram Walkthrough ```mermaid flowchart LR ML["MainLayout"] -- "click Alerts" --> AL["AlertList.vue"] ML -- "click Incidents" --> IL["IncidentList.vue"] AL -- "imports IncidentList removed" --> IL ``` <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>4 files</summary><table> <tr> <td><strong>router.ts</strong><dd><code>Add dedicated route for IncidentList.vue</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-a1827956e5a817edc7efb5f4516e7cc4d453e15ea9e9d970aafb8360b09fc78e">+12/-0</a> </td> </tr> <tr> <td><strong>AlertList.vue</strong><dd><code>Remove incidents tab and adjust layout</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-be3e41b823e163b226ffc270d95cc4981e59c600851e4e0d10ee5e4ee994ac8e">+14/-102</a></td> </tr> <tr> <td><strong>IncidentList.vue</strong><dd><code>Refactor IncidentList with header and search</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-953ad015996758a8c86e489499cd3b0a614501b99b0c9c5c242a299e34c24ff2">+54/-28</a> </td> </tr> <tr> <td><strong>MainLayout.vue</strong><dd><code>Add incidents link to main menu</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-91e4134141264c4624d4eb4cfa987b33ed31eb6f551e089c0b26f2610dd54362">+11/-4</a> </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>11 files</summary><table> <tr> <td><strong>de.json</strong><dd><code>Add German translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-78f24ecb2dbcfc38fdf3ec22b2559a5b40d236ea689d1c55f5d3e87168310264">+2/-1</a> </td> </tr> <tr> <td><strong>en.json</strong><dd><code>Add English translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-892aa3d75ab6e129ef9116aa21bec1b9a15ba69ce5366d2a19128078c690d824">+2/-0</a> </td> </tr> <tr> <td><strong>es.json</strong><dd><code>Add Spanish translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-ac2a2167b8b973e3f776f9fe3b43bef4576470d8ad33f3d7cfaff6edff0e5778">+2/-1</a> </td> </tr> <tr> <td><strong>fr.json</strong><dd><code>Add French translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-db80e762b88ddf183820c050cdc6b215e6b02b365b5e73f5152af46b0d26f1a2">+2/-1</a> </td> </tr> <tr> <td><strong>it.json</strong><dd><code>Add Italian translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-2de78074a5f887afddce60d694ead9322e6ab223eb70251c13819f3028bdc70d">+2/-1</a> </td> </tr> <tr> <td><strong>ja.json</strong><dd><code>Add Japanese translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-092e49c5f9fbc1b539ab9c2866d108d0ce1d314022b4c294eb4611b285e7bbd3">+2/-1</a> </td> </tr> <tr> <td><strong>ko.json</strong><dd><code>Add Korean translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-50e27ca1b4825a5026f3a251bfcf79c97cab07cfade2ebd512d332453d707c48">+2/-1</a> </td> </tr> <tr> <td><strong>nl.json</strong><dd><code>Add Dutch translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-a1706021687ae5d8a1c006c49eab3946174fecd895f9b70a89c2e76d97250906">+2/-1</a> </td> </tr> <tr> <td><strong>pt.json</strong><dd><code>Add Portuguese translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-38e804f64cb5e0e451849fc84e9b46e53c954a4a4263d51c18665ad45f4b4815">+2/-1</a> </td> </tr> <tr> <td><strong>tr.json</strong><dd><code>Add Turkish translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-6f0f131e9e2591480c4c5d1eaffc72d02dc31cbd8d3f90d8f012f08cabac8818">+2/-1</a> </td> </tr> <tr> <td><strong>zh.json</strong><dd><code>Add Chinese translation for incidents</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10080/files#diff-8818055db58f83230aeea4ebfd4cec31d924e8c47b6f91298e356959ab25339e">+2/-1</a> </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___ --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
github-actions[bot]
parent
c3793677b5
commit
99c0221126
@@ -293,12 +293,10 @@ const waitData = async (wrapper: any) => {
|
||||
|
||||
// 1. Basic rendering and structure
|
||||
describe("AlertList - basic rendering", () => {
|
||||
it("renders the page container and view mode tabs", async () => {
|
||||
it("renders the page container", async () => {
|
||||
const wrapper = await mountAlertList();
|
||||
await waitData(wrapper);
|
||||
expect(wrapper.find('[data-test="alert-list-page"]').exists()).toBe(true);
|
||||
// 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 () => {
|
||||
|
||||
@@ -21,33 +21,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
data-test="alert-list-page"
|
||||
class="q-pa-none flex flex-col"
|
||||
>
|
||||
<div class="tw:w-full tw:h-[68px] tw:px-[0.625rem] tw:mb-[0.375rem] q-pt-xs" v-if="!showAddAlertDialog && !showImportAlertDialog">
|
||||
<div class="tw:w-full tw:px-[0.625rem] q-pt-xs" v-if="!showAddAlertDialog && !showImportAlertDialog">
|
||||
<div class="card-container">
|
||||
<div
|
||||
class="flex justify-between full-width tw:py-3 tw:px-4 items-center"
|
||||
class="flex justify-between full-width tw:py-3 tw:mb-[0.625rem] tw:px-4 tw:h-[68px] items-center"
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-4">
|
||||
<!-- View Mode Tabs (Alerts / Incidents) -->
|
||||
<div class="flex justify-start">
|
||||
<q-tabs v-model="viewMode" inline-label dense @update:model-value="onViewModeChange" data-test="alert-incident-view-tabs">
|
||||
<q-tab
|
||||
name="alerts"
|
||||
:label="t('alerts.header')"
|
||||
no-caps
|
||||
data-test="alert-view-tab"
|
||||
/>
|
||||
<q-tab
|
||||
name="incidents"
|
||||
:label="t('alerts.incidents.title')"
|
||||
no-caps
|
||||
data-test="incident-view-tab"
|
||||
/>
|
||||
</q-tabs>
|
||||
<div class="q-table__title tw:font-[600]" data-test="alert-list-title">
|
||||
{{ t("alerts.header") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex q-ml-auto tw:ps-2 items-center">
|
||||
<!-- Tabs only visible in Alerts view -->
|
||||
<div v-if="viewMode === 'alerts'" class="app-tabs-container tw:h-[36px] q-mr-sm">
|
||||
<!-- Alert Tabs -->
|
||||
<div class="app-tabs-container tw:h-[36px] q-mr-sm">
|
||||
<app-tabs
|
||||
class="tabs-selection-container"
|
||||
:tabs="alertTabs"
|
||||
@@ -55,9 +41,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@update:active-tab="filterAlertsByTab"
|
||||
/>
|
||||
</div>
|
||||
<!-- Search for Alerts view -->
|
||||
<!-- Search for Alerts -->
|
||||
<q-input
|
||||
v-if="viewMode === 'alerts'"
|
||||
v-model="dynamicQueryModel"
|
||||
dense
|
||||
borderless
|
||||
@@ -75,23 +60,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<q-icon class="o2-search-input-icon" name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
<!-- Search for Incidents view -->
|
||||
<q-input
|
||||
v-if="viewMode === 'incidents'"
|
||||
v-model="incidentSearchQuery"
|
||||
dense
|
||||
borderless
|
||||
placeholder="Search incidents..."
|
||||
data-test="incident-search-input"
|
||||
clearable
|
||||
class="o2-search-input"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon class="o2-search-input-icon" name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
<!-- All Folders toggle (only for alerts view) -->
|
||||
<div v-if="viewMode === 'alerts'" class="tw:ml-2">
|
||||
<!-- All Folders toggle -->
|
||||
<div class="tw:ml-2">
|
||||
<q-toggle
|
||||
data-test="alert-list-search-across-folders-toggle"
|
||||
v-model="searchAcrossFolders"
|
||||
@@ -119,9 +89,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
data-test="alert-insights-btn"
|
||||
icon="insights"
|
||||
/>
|
||||
<!-- Import button (only for alerts view) -->
|
||||
<!-- Import button -->
|
||||
<q-btn
|
||||
v-if="viewMode === 'alerts'"
|
||||
class="q-ml-sm o2-secondary-button tw:h-[36px]"
|
||||
no-caps
|
||||
flat
|
||||
@@ -129,9 +98,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@click="importAlert"
|
||||
data-test="alert-import"
|
||||
/>
|
||||
<!-- Add Alert button (only for alerts view) -->
|
||||
<!-- Add Alert button -->
|
||||
<q-btn
|
||||
v-if="viewMode === 'alerts'"
|
||||
data-test="alert-list-add-alert-btn"
|
||||
class="q-ml-sm o2-primary-button tw:h-[36px]"
|
||||
no-caps
|
||||
@@ -147,20 +115,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div
|
||||
v-if="!showAddAlertDialog && !showImportAlertDialog"
|
||||
class="full-width alert-list-table"
|
||||
style="height: calc(100vh - 110px)"
|
||||
style="height: calc(100vh - 118px)"
|
||||
>
|
||||
<!-- Incidents View (no folders) -->
|
||||
<div v-if="viewMode === 'incidents'" class="tw:w-full tw:h-full tw:pr-[0.625rem] tw:pb-[0.625rem]">
|
||||
<IncidentList :searchQuery="incidentSearchQuery" />
|
||||
</div>
|
||||
|
||||
<!-- Alerts View (with folders) -->
|
||||
<q-splitter
|
||||
v-else
|
||||
v-model="splitterModel"
|
||||
unit="px"
|
||||
:limits="[200, 500]"
|
||||
style="height: calc(100vh - 110px)"
|
||||
style="height: calc(100vh - 118px)"
|
||||
data-test="alert-list-splitter"
|
||||
>
|
||||
<template #before>
|
||||
@@ -912,7 +874,6 @@ import AppTabs from "@/components/common/AppTabs.vue";
|
||||
import SelectFolderDropDown from "../common/sidebar/SelectFolderDropDown.vue";
|
||||
import AlertHistoryDrawer from "@/components/alerts/AlertHistoryDrawer.vue";
|
||||
import { symOutlinedSoundSampler } from "@quasar/extras/material-symbols-outlined";
|
||||
import IncidentList from "@/components/alerts/IncidentList.vue";
|
||||
import O2AIContextAddBtn from "@/components/common/O2AIContextAddBtn.vue";
|
||||
import { buildConditionsString } from "@/utils/alerts/conditionsFormatter";
|
||||
// import alertList from "./alerts";
|
||||
@@ -933,7 +894,6 @@ export default defineComponent({
|
||||
AppTabs,
|
||||
SelectFolderDropDown,
|
||||
AlertHistoryDrawer,
|
||||
IncidentList,
|
||||
O2AIContextAddBtn,
|
||||
},
|
||||
emits: [
|
||||
@@ -1143,32 +1103,12 @@ export default defineComponent({
|
||||
|
||||
const activeFolderToMove = ref("default");
|
||||
|
||||
// Initialize viewMode from URL query parameter
|
||||
const viewMode = ref(
|
||||
(router.currentRoute.value.query.view as string) === "incidents" ? "incidents" : "alerts"
|
||||
);
|
||||
|
||||
// Initialize activeTab from URL query parameter, default to "all"
|
||||
const activeTab = ref(
|
||||
(router.currentRoute.value.query.tab as string) || "all"
|
||||
);
|
||||
|
||||
// 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)
|
||||
// Tabs for alerts view
|
||||
const alertTabs = reactive([
|
||||
{
|
||||
label: t("alerts.all"),
|
||||
@@ -1198,32 +1138,8 @@ export default defineComponent({
|
||||
label: t("alerts.realTime"),
|
||||
value: "realTime",
|
||||
},
|
||||
{
|
||||
label: t("alerts.incidents.title"),
|
||||
value: "incidents",
|
||||
},
|
||||
]);
|
||||
|
||||
const onViewModeChange = (newMode: string) => {
|
||||
// Update viewMode immediately
|
||||
viewMode.value = newMode;
|
||||
|
||||
// Update URL query parameter
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
view: newMode,
|
||||
// Reset tab to 'all' when switching to alerts view
|
||||
tab: newMode === "alerts" ? "all" : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Reset active tab when switching to alerts view
|
||||
if (newMode === "alerts") {
|
||||
activeTab.value = "all";
|
||||
}
|
||||
};
|
||||
|
||||
const columns = computed(() => {
|
||||
const baseColumns: any = [
|
||||
{
|
||||
@@ -2781,10 +2697,6 @@ export default defineComponent({
|
||||
computedOwner,
|
||||
tabs,
|
||||
alertTabs,
|
||||
viewMode,
|
||||
viewTabs,
|
||||
onViewModeChange,
|
||||
incidentSearchQuery,
|
||||
filterAlertsByTab,
|
||||
showHistoryDrawer,
|
||||
selectedHistoryAlertId,
|
||||
|
||||
@@ -79,11 +79,14 @@ describe("IncidentDetailDrawer.vue", () => {
|
||||
Object.assign(store.state, storeOverrides);
|
||||
}
|
||||
|
||||
// Set incident_id in router query if provided
|
||||
// Set incident ID in router params if provided (changed from query to params)
|
||||
if (incidentId) {
|
||||
await router.push({ query: { incident_id: incidentId } });
|
||||
} else {
|
||||
await router.push({ query: {} });
|
||||
// Set route params directly to avoid async router.push hanging
|
||||
router.currentRoute.value = {
|
||||
...router.currentRoute.value,
|
||||
params: { id: incidentId },
|
||||
name: "incidentDetail",
|
||||
} as any;
|
||||
}
|
||||
|
||||
return mount(IncidentDetailDrawer, {
|
||||
@@ -93,9 +96,10 @@ describe("IncidentDetailDrawer.vue", () => {
|
||||
global: {
|
||||
plugins: [i18n, store, router],
|
||||
stubs: {
|
||||
QDrawer: true,
|
||||
QPage: true,
|
||||
TelemetryCorrelationDashboard: true,
|
||||
IncidentServiceGraph: true,
|
||||
SREChat: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -179,11 +183,13 @@ describe("IncidentDetailDrawer.vue", () => {
|
||||
describe("URL-based Incident Loading", () => {
|
||||
it("should emit close when drawer closes", async () => {
|
||||
wrapper = await createWrapper();
|
||||
const pushSpy = vi.spyOn(router, 'push');
|
||||
|
||||
wrapper.vm.close();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted("close")).toBeTruthy();
|
||||
// Should navigate back to incident list instead of emitting
|
||||
expect(pushSpy).toHaveBeenCalledWith({ name: "incidentList" });
|
||||
});
|
||||
|
||||
it("should load details when incident_id is in URL", async () => {
|
||||
@@ -334,10 +340,13 @@ describe("IncidentDetailDrawer.vue", () => {
|
||||
expect(mockNotify.mock.calls[0][0].type).toBe("positive");
|
||||
});
|
||||
|
||||
it("should emit status-updated event", async () => {
|
||||
it("should not emit status-updated event (removed)", async () => {
|
||||
// This test is no longer relevant as the component doesn't emit events
|
||||
// The component now handles its own state updates
|
||||
await wrapper.vm.acknowledgeIncident();
|
||||
|
||||
expect(wrapper.emitted("status-updated")).toBeTruthy();
|
||||
// Just verify the API was called
|
||||
expect(incidentsService.updateStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle status update error", async () => {
|
||||
@@ -482,35 +491,49 @@ describe("IncidentDetailDrawer.vue", () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("should open SRE chat with incident context", () => {
|
||||
wrapper.vm.openSREChat();
|
||||
|
||||
expect(store.state.sreChatContext).toBeTruthy();
|
||||
expect(store.state.sreChatContext.type).toBe("incident");
|
||||
});
|
||||
|
||||
it("should dispatch setIsSREChatOpen action", () => {
|
||||
const dispatchSpy = vi.spyOn(store, 'dispatch');
|
||||
it("should toggle SRE chat visibility", () => {
|
||||
expect(wrapper.vm.showAIChat).toBe(false);
|
||||
|
||||
wrapper.vm.openSREChat();
|
||||
expect(wrapper.vm.showAIChat).toBe(true);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith("setIsSREChatOpen", true);
|
||||
});
|
||||
|
||||
it("should pass incident details to chat", () => {
|
||||
wrapper.vm.openSREChat();
|
||||
|
||||
expect(store.state.sreChatContext.data.id).toBe("1");
|
||||
expect(store.state.sreChatContext.data.severity).toBe("P1");
|
||||
expect(wrapper.vm.showAIChat).toBe(false);
|
||||
});
|
||||
|
||||
it("should include triggers in chat context", () => {
|
||||
wrapper.vm.openSREChat();
|
||||
it("should compute incident context data correctly", () => {
|
||||
const contextData = wrapper.vm.incidentContextData;
|
||||
|
||||
expect(store.state.sreChatContext.data.triggers).toBeTruthy();
|
||||
expect(contextData).toBeTruthy();
|
||||
expect(contextData.id).toBe("1");
|
||||
expect(contextData.severity).toBe("P1");
|
||||
expect(contextData.status).toBe("open");
|
||||
});
|
||||
|
||||
it("should include existing RCA in chat context", () => {
|
||||
it("should include all required fields in context data", () => {
|
||||
const contextData = wrapper.vm.incidentContextData;
|
||||
|
||||
expect(contextData).toHaveProperty("id");
|
||||
expect(contextData).toHaveProperty("title");
|
||||
expect(contextData).toHaveProperty("status");
|
||||
expect(contextData).toHaveProperty("severity");
|
||||
expect(contextData).toHaveProperty("alert_count");
|
||||
expect(contextData).toHaveProperty("first_alert_at");
|
||||
expect(contextData).toHaveProperty("last_alert_at");
|
||||
expect(contextData).toHaveProperty("stable_dimensions");
|
||||
expect(contextData).toHaveProperty("topology_context");
|
||||
expect(contextData).toHaveProperty("triggers");
|
||||
expect(contextData).toHaveProperty("rca_analysis");
|
||||
});
|
||||
|
||||
it("should include triggers in context data", () => {
|
||||
const contextData = wrapper.vm.incidentContextData;
|
||||
|
||||
expect(contextData.triggers).toBeTruthy();
|
||||
expect(Array.isArray(contextData.triggers)).toBe(true);
|
||||
});
|
||||
|
||||
it("should include existing RCA in context data when available", async () => {
|
||||
wrapper.vm.incidentDetails.topology_context = {
|
||||
service: "api",
|
||||
upstream_services: [],
|
||||
@@ -519,17 +542,20 @@ describe("IncidentDetailDrawer.vue", () => {
|
||||
suggested_root_cause: "Existing RCA",
|
||||
};
|
||||
|
||||
wrapper.vm.openSREChat();
|
||||
await nextTick();
|
||||
|
||||
expect(store.state.sreChatContext.data.rca_analysis).toBe("Existing RCA");
|
||||
const contextData = wrapper.vm.incidentContextData;
|
||||
expect(contextData.rca_analysis).toBe("Existing RCA");
|
||||
});
|
||||
|
||||
it("should include stream content when no existing RCA", () => {
|
||||
it("should include stream content when no existing RCA", async () => {
|
||||
wrapper.vm.rcaStreamContent = "Streaming RCA content";
|
||||
wrapper.vm.incidentDetails.topology_context = undefined;
|
||||
|
||||
wrapper.vm.openSREChat();
|
||||
await nextTick();
|
||||
|
||||
expect(store.state.sreChatContext.data.rca_analysis).toBe("Streaming RCA content");
|
||||
const contextData = wrapper.vm.incidentContextData;
|
||||
expect(contextData.rca_analysis).toBe("Streaming RCA content");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -881,20 +907,24 @@ describe("IncidentDetailDrawer.vue", () => {
|
||||
describe("Close Functionality", () => {
|
||||
it("should close drawer", async () => {
|
||||
wrapper = await createWrapper();
|
||||
const pushSpy = vi.spyOn(router, 'push');
|
||||
|
||||
wrapper.vm.close();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted("close")).toBeTruthy();
|
||||
// Should navigate back to incident list
|
||||
expect(pushSpy).toHaveBeenCalledWith({ name: "incidentList" });
|
||||
});
|
||||
|
||||
it("should emit update:modelValue on close", async () => {
|
||||
it("should navigate back to incident list on close", async () => {
|
||||
wrapper = await createWrapper();
|
||||
const pushSpy = vi.spyOn(router, 'push');
|
||||
|
||||
wrapper.vm.close();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted("close")).toBeTruthy();
|
||||
// Verify navigation to incident list
|
||||
expect(pushSpy).toHaveBeenCalledWith({ name: "incidentList" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,98 +15,112 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[store.state.theme === 'dark' ? 'bg-dark' : 'bg-white']"
|
||||
class="tw:h-full tw:flex tw:flex-col"
|
||||
style="width: 94vw"
|
||||
>
|
||||
<q-page data-test="incident-detail-page" class="q-pa-none" style="height: calc(100vh - 50px); overflow: hidden;">
|
||||
<div class="tw:w-full tw:h-full tw:px-[0.625rem] q-mt-xs tw:pb-[0.625rem]">
|
||||
<!-- Header -->
|
||||
<div class="incident-detail-header row items-center no-wrap q-px-md tw:p-4">
|
||||
<div class="col">
|
||||
<div class="tw:flex tw:items-center tw:gap-3 tw:flex-wrap">
|
||||
<div class="tw:text-[18px]">
|
||||
{{ t("alerts.incidents.header") }}
|
||||
</div>
|
||||
<!-- Incident name with colored indicator -->
|
||||
<span
|
||||
:class="[
|
||||
'tw:font-bold tw:px-2 tw:py-1 tw:rounded-md tw:max-w-xs tw:truncate tw:inline-block',
|
||||
store.state.theme === 'dark'
|
||||
? 'tw:text-blue-400 tw:bg-blue-900/50'
|
||||
: 'tw:text-blue-600 tw:bg-blue-50'
|
||||
]"
|
||||
data-test="incident-detail-title"
|
||||
<div class="row items-center no-wrap card-container tw:py-[0.675rem] tw:h-[68px] tw:px-[0.675rem] tw:mb-[0.675rem]">
|
||||
<div class="flex items-center tw:gap-3 tw:flex-1">
|
||||
<div
|
||||
data-test="incident-detail-back-btn"
|
||||
class="flex justify-center items-center q-mr-md cursor-pointer"
|
||||
style="
|
||||
border: 1.5px solid;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
"
|
||||
title="Go Back"
|
||||
@click="close"
|
||||
>
|
||||
<q-icon name="arrow_back_ios_new" size="14px" />
|
||||
</div>
|
||||
<div class="text-h6">
|
||||
{{ t("alerts.incidents.header") }}
|
||||
</div>
|
||||
<!-- Incident name with colored indicator -->
|
||||
<span
|
||||
v-if="incidentDetails"
|
||||
:class="[
|
||||
'tw:font-bold tw:px-2 tw:py-1 tw:rounded-md tw:max-w-xs tw:truncate tw:inline-block',
|
||||
store.state.theme === 'dark'
|
||||
? 'tw:text-blue-400 tw:bg-blue-900/50'
|
||||
: 'tw:text-blue-600 tw:bg-blue-50'
|
||||
]"
|
||||
data-test="incident-detail-title"
|
||||
>
|
||||
{{ incidentDetails.title }}
|
||||
<q-tooltip v-if="incidentDetails && incidentDetails.title.length > 35" class="tw:text-xs">
|
||||
{{ incidentDetails.title }}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
|
||||
<!-- Compact Status, Severity, Alerts badges -->
|
||||
<div v-if="incidentDetails" class="tw:flex tw:items-center tw:gap-2">
|
||||
<!-- Status Badge -->
|
||||
<q-badge
|
||||
:color="getStatusColor(incidentDetails.status)"
|
||||
class="tw:px-2.5 tw:py-1.5 tw:cursor-default"
|
||||
outline
|
||||
>
|
||||
{{ incidentDetails?.title }}
|
||||
<q-tooltip v-if="incidentDetails && incidentDetails.title.length > 35" class="tw:text-xs">
|
||||
{{ incidentDetails.title }}
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<q-icon name="info" size="14px" />
|
||||
<span>{{ getStatusLabel(incidentDetails.status) }}</span>
|
||||
</div>
|
||||
<q-tooltip :delay="200" class="tw:text-xs">
|
||||
{{ t("alerts.incidents.status") }}: {{ getStatusLabel(incidentDetails.status) }}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
</q-badge>
|
||||
|
||||
<!-- Compact Status, Severity, Alerts badges -->
|
||||
<div v-if="incidentDetails" class="tw:flex tw:items-center tw:gap-2">
|
||||
<!-- Status Badge -->
|
||||
<q-badge
|
||||
:color="getStatusColor(incidentDetails.status)"
|
||||
class="tw:px-2.5 tw:py-1.5 tw:cursor-default"
|
||||
outline
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<q-icon name="info" size="14px" />
|
||||
<span>{{ getStatusLabel(incidentDetails.status) }}</span>
|
||||
</div>
|
||||
<q-tooltip :delay="200" class="tw:text-xs">
|
||||
{{ t("alerts.incidents.status") }}: {{ getStatusLabel(incidentDetails.status) }}
|
||||
</q-tooltip>
|
||||
</q-badge>
|
||||
<!-- Severity Badge -->
|
||||
<q-badge
|
||||
:style="{ backgroundColor: getSeverityColorHex(incidentDetails.severity), color: '#fff' }"
|
||||
class="tw:px-2.5 tw:py-1.5 tw:cursor-default"
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<q-icon name="warning" size="14px" />
|
||||
<span>{{ incidentDetails.severity }}</span>
|
||||
</div>
|
||||
<q-tooltip :delay="200" class="tw:text-xs">
|
||||
{{ t("alerts.incidents.severity") }}: {{ incidentDetails.severity }}
|
||||
</q-tooltip>
|
||||
</q-badge>
|
||||
|
||||
<!-- Severity Badge -->
|
||||
<q-badge
|
||||
:style="{ backgroundColor: getSeverityColorHex(incidentDetails.severity), color: '#fff' }"
|
||||
class="tw:px-2.5 tw:py-1.5 tw:cursor-default"
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<q-icon name="warning" size="14px" />
|
||||
<span>{{ incidentDetails.severity }}</span>
|
||||
</div>
|
||||
<q-tooltip :delay="200" class="tw:text-xs">
|
||||
{{ t("alerts.incidents.severity") }}: {{ incidentDetails.severity }}
|
||||
</q-tooltip>
|
||||
</q-badge>
|
||||
|
||||
<!-- Alert Count Badge -->
|
||||
<q-badge
|
||||
color="primary"
|
||||
class="tw:px-2.5 tw:py-1.5 tw:cursor-default"
|
||||
outline
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<q-icon name="notifications_active" size="14px" />
|
||||
<span>{{ incidentDetails.alert_count }} Alerts</span>
|
||||
</div>
|
||||
<q-tooltip :delay="200" class="tw:text-xs">
|
||||
{{ t("alerts.incidents.alertCount") }}: {{ incidentDetails.alert_count }} correlated alerts
|
||||
</q-tooltip>
|
||||
</q-badge>
|
||||
</div>
|
||||
<!-- Alert Count Badge -->
|
||||
<q-badge
|
||||
color="primary"
|
||||
class="tw:px-2.5 tw:py-1.5 tw:cursor-default"
|
||||
outline
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<q-icon name="notifications_active" size="14px" />
|
||||
<span>{{ incidentDetails.alert_count }} Alerts</span>
|
||||
</div>
|
||||
<q-tooltip :delay="200" class="tw:text-xs">
|
||||
{{ t("alerts.incidents.alertCount") }}: {{ incidentDetails.alert_count }} correlated alerts
|
||||
</q-tooltip>
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
data-test="incident-detail-close-btn"
|
||||
@click="close"
|
||||
round
|
||||
flat
|
||||
icon="cancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
|
||||
<!-- Content -->
|
||||
<div v-if="!loading && incidentDetails" class="tw:flex-1 tw:flex tw:overflow-hidden">
|
||||
<div v-if="!loading && incidentDetails" class="card-container tw:flex tw:overflow-hidden" style="height: calc(100vh - 130px);">
|
||||
<!-- AI Chat Panel (conditionally shown on the right) -->
|
||||
<div
|
||||
v-if="showAIChat"
|
||||
class="ai-chat-panel tw:w-[380px] tw:flex-shrink-0 tw:flex tw:flex-col tw:overflow-hidden"
|
||||
:class="store.state.theme === 'dark' ? 'tw:border-l tw:border-gray-700' : 'tw:border-l tw:border-gray-200'"
|
||||
style="order: 3;"
|
||||
>
|
||||
<SREChat
|
||||
context-type="incident"
|
||||
:context-data="incidentContextData"
|
||||
@close="closeAIChat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Left Column: Incident Details -->
|
||||
<div class="incident-details-column tw:w-[400px] tw:flex-shrink-0 tw:flex tw:flex-col" :class="store.state.theme === 'dark' ? 'tw:border-r tw:border-gray-700' : 'tw:border-r tw:border-gray-200'">
|
||||
<div class="incident-details-column tw:w-[400px] tw:flex-shrink-0 tw:flex tw:flex-col" :class="store.state.theme === 'dark' ? 'tw:border-r tw:border-gray-700' : 'tw:border-r tw:border-gray-200'" style="order: 1;">
|
||||
|
||||
<!-- Top Section (52% height) - Table of Contents -->
|
||||
<div
|
||||
@@ -440,49 +454,103 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Tabs and Content -->
|
||||
<div class="tabs-content-column tw:flex-1 tw:flex tw:flex-col tw:overflow-hidden">
|
||||
<div class="tabs-content-column tw:flex-1 tw:flex tw:flex-col tw:overflow-hidden" style="order: 2;">
|
||||
<!-- Tabs -->
|
||||
<div class="tw:flex tw:items-center tw:justify-between tw:px-4 tw:pt-4 tw:pb-2 q-px-md tw:flex-shrink-0">
|
||||
<q-tabs v-model="activeTab" inline-label dense no-caps align="left">
|
||||
<q-tab
|
||||
name="incidentAnalysis"
|
||||
icon="psychology"
|
||||
label="Incident Analysis"
|
||||
/>
|
||||
<q-tab
|
||||
name="serviceGraph"
|
||||
icon="hub"
|
||||
label="Alert Graph"
|
||||
/>
|
||||
<q-tab
|
||||
name="alertTriggers"
|
||||
icon="notifications_active"
|
||||
>
|
||||
<template #default>
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<span>Alert Triggers</span>
|
||||
<span class="tw:text-xs tw:opacity-70">({{ triggers.length }})</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-tab>
|
||||
<div class="tw:flex tw:items-center tw:gap-2">
|
||||
<q-tabs v-model="activeTab" inline-label dense no-caps align="left">
|
||||
<q-tab
|
||||
name="incidentAnalysis"
|
||||
icon="psychology"
|
||||
label="Incident Analysis"
|
||||
/>
|
||||
<q-tab
|
||||
name="serviceGraph"
|
||||
icon="hub"
|
||||
label="Alert Graph"
|
||||
/>
|
||||
<q-tab
|
||||
name="alertTriggers"
|
||||
icon="notifications_active"
|
||||
>
|
||||
<template #default>
|
||||
<div class="tw:flex tw:items-center tw:gap-1.5">
|
||||
<span>Alert Triggers</span>
|
||||
<span class="tw:text-xs tw:opacity-70">({{ triggers.length }})</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-tab>
|
||||
|
||||
<!-- New Telemetry Tabs -->
|
||||
<q-tab
|
||||
name="logs"
|
||||
icon="description"
|
||||
:label="t('common.logs')"
|
||||
/>
|
||||
<q-tab
|
||||
name="metrics"
|
||||
icon="bar_chart"
|
||||
:label="t('search.metrics')"
|
||||
/>
|
||||
<q-tab
|
||||
name="traces"
|
||||
icon="timeline"
|
||||
:label="t('menu.traces')"
|
||||
/>
|
||||
</q-tabs>
|
||||
<!-- Show telemetry tabs inline when AI chat is closed -->
|
||||
<q-tab
|
||||
v-if="!showAIChat"
|
||||
name="logs"
|
||||
icon="description"
|
||||
:label="t('common.logs')"
|
||||
/>
|
||||
<q-tab
|
||||
v-if="!showAIChat"
|
||||
name="metrics"
|
||||
icon="bar_chart"
|
||||
:label="t('search.metrics')"
|
||||
/>
|
||||
<q-tab
|
||||
v-if="!showAIChat"
|
||||
name="traces"
|
||||
icon="timeline"
|
||||
:label="t('menu.traces')"
|
||||
/>
|
||||
</q-tabs>
|
||||
|
||||
<!-- Telemetry Dropdown Menu - Only show when AI chat is open -->
|
||||
<q-btn
|
||||
v-if="showAIChat"
|
||||
flat
|
||||
dense
|
||||
icon="menu"
|
||||
size="sm"
|
||||
class="tw:ml-2"
|
||||
>
|
||||
<q-tooltip :delay="500">View Telemetry Data</q-tooltip>
|
||||
<q-menu>
|
||||
<q-list dense style="min-width: 150px;">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="activeTab = 'logs'"
|
||||
:class="activeTab === 'logs' ? 'bg-primary text-white' : ''"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="description" size="18px" />
|
||||
</q-item-section>
|
||||
<q-item-section>{{ t('common.logs') }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="activeTab = 'metrics'"
|
||||
:class="activeTab === 'metrics' ? 'bg-primary text-white' : ''"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="bar_chart" size="18px" />
|
||||
</q-item-section>
|
||||
<q-item-section>{{ t('search.metrics') }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="activeTab = 'traces'"
|
||||
:class="activeTab === 'traces' ? 'bg-primary text-white' : ''"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="timeline" size="18px" />
|
||||
</q-item-section>
|
||||
<q-item-section>{{ t('menu.traces') }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="tw:flex tw:gap-2 tw:ml-auto tw:items-center">
|
||||
@@ -495,10 +563,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
dense
|
||||
@click="acknowledgeIncident"
|
||||
:loading="updating"
|
||||
class="tw:px-3"
|
||||
class="tw:px-2"
|
||||
>
|
||||
<q-icon name="check_circle" size="16px" class="tw:mr-1" />
|
||||
{{ t("alerts.incidents.acknowledge") }}
|
||||
<q-icon name="check_circle" size="16px" :class="showAIChat ? '' : 'tw:mr-1'" />
|
||||
<span v-if="!showAIChat">{{ t("alerts.incidents.acknowledge") }}</span>
|
||||
<q-tooltip :delay="500">Mark incident as acknowledged and being worked on</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@@ -510,10 +578,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
dense
|
||||
@click="resolveIncident"
|
||||
:loading="updating"
|
||||
class="tw:px-3"
|
||||
class="tw:px-2"
|
||||
>
|
||||
<q-icon name="task_alt" size="16px" class="tw:mr-1" />
|
||||
{{ t("alerts.incidents.resolve") }}
|
||||
<q-icon name="task_alt" size="16px" :class="showAIChat ? '' : 'tw:mr-1'" />
|
||||
<span v-if="!showAIChat">{{ t("alerts.incidents.resolve") }}</span>
|
||||
<q-tooltip :delay="500">Mark incident as resolved and close it</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@@ -525,10 +593,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
dense
|
||||
@click="reopenIncident"
|
||||
:loading="updating"
|
||||
class="tw:px-3"
|
||||
class="tw:px-2"
|
||||
>
|
||||
<q-icon name="refresh" size="16px" class="tw:mr-1" />
|
||||
{{ t("alerts.incidents.reopen") }}
|
||||
<q-icon name="refresh" size="16px" :class="showAIChat ? '' : 'tw:mr-1'" />
|
||||
<span v-if="!showAIChat">{{ t("alerts.incidents.reopen") }}</span>
|
||||
<q-tooltip :delay="500">Reopen this resolved incident</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
@@ -851,6 +919,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<q-spinner-hourglass size="lg" color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -871,15 +940,16 @@ import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import TelemetryCorrelationDashboard from "@/plugins/correlation/TelemetryCorrelationDashboard.vue";
|
||||
import IncidentServiceGraph from "./IncidentServiceGraph.vue";
|
||||
import SREChat from "@/components/SREChat.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "IncidentDetailDrawer",
|
||||
components: {
|
||||
TelemetryCorrelationDashboard,
|
||||
IncidentServiceGraph,
|
||||
SREChat,
|
||||
},
|
||||
emits: ["close", "status-updated"],
|
||||
setup(_props, { emit }) {
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const $q = useQuasar();
|
||||
@@ -892,6 +962,7 @@ export default defineComponent({
|
||||
const alerts = ref<any[]>([]);
|
||||
const rcaLoading = ref(false);
|
||||
const rcaStreamContent = ref("");
|
||||
const showAIChat = ref(false);
|
||||
|
||||
// Tab management
|
||||
const activeTab = ref("incidentAnalysis");
|
||||
@@ -967,6 +1038,27 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
// Computed property for SRE chat incident context
|
||||
const incidentContextData = computed(() => {
|
||||
if (!incidentDetails.value) return null;
|
||||
|
||||
return {
|
||||
id: incidentDetails.value.id,
|
||||
title: incidentDetails.value.title,
|
||||
status: incidentDetails.value.status,
|
||||
severity: incidentDetails.value.severity,
|
||||
alert_count: incidentDetails.value.alert_count,
|
||||
first_alert_at: incidentDetails.value.first_alert_at,
|
||||
last_alert_at: incidentDetails.value.last_alert_at,
|
||||
stable_dimensions: incidentDetails.value.stable_dimensions,
|
||||
topology_context: incidentDetails.value.topology_context,
|
||||
triggers: triggers.value,
|
||||
rca_analysis: hasExistingRca.value
|
||||
? incidentDetails.value?.topology_context?.suggested_root_cause
|
||||
: rcaStreamContent.value,
|
||||
};
|
||||
});
|
||||
|
||||
// Computed properties for TelemetryCorrelationDashboard
|
||||
const telemetryTimeRange = computed(() => {
|
||||
if (!incidentDetails.value) {
|
||||
@@ -1028,12 +1120,12 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for URL query parameter
|
||||
// Watch for URL route parameter
|
||||
watch(
|
||||
() => router.currentRoute.value.query.incident_id,
|
||||
() => router.currentRoute.value.params.id,
|
||||
(incidentIdFromUrl) => {
|
||||
if (incidentIdFromUrl && typeof incidentIdFromUrl === 'string') {
|
||||
// Load incident from URL query parameter
|
||||
// Load incident from URL route parameter
|
||||
// Validate incident ID format (e.g., UUID validation)
|
||||
if (incidentIdFromUrl.trim().length > 0) {
|
||||
loadDetails(incidentIdFromUrl);
|
||||
@@ -1055,7 +1147,9 @@ export default defineComponent({
|
||||
// Clear correlation data when closing
|
||||
correlationData.value = null;
|
||||
correlationError.value = null;
|
||||
emit("close");
|
||||
|
||||
// Navigate back to incident list
|
||||
router.push({ name: "incidentList" });
|
||||
};
|
||||
|
||||
const updateStatus = async (newStatus: "open" | "acknowledged" | "resolved") => {
|
||||
@@ -1593,31 +1687,12 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const openSREChat = () => {
|
||||
// Set SRE chat context with full incident context
|
||||
store.state.sreChatContext = {
|
||||
type: 'incident',
|
||||
data: {
|
||||
id: incidentDetails.value?.id,
|
||||
title: incidentDetails.value?.title,
|
||||
status: incidentDetails.value?.status,
|
||||
severity: incidentDetails.value?.severity,
|
||||
alert_count: incidentDetails.value?.alert_count,
|
||||
first_alert_at: incidentDetails.value?.first_alert_at,
|
||||
last_alert_at: incidentDetails.value?.last_alert_at,
|
||||
stable_dimensions: incidentDetails.value?.stable_dimensions,
|
||||
topology_context: incidentDetails.value?.topology_context,
|
||||
triggers: triggers.value,
|
||||
rca_analysis: hasExistingRca.value
|
||||
? incidentDetails.value?.topology_context?.suggested_root_cause
|
||||
: rcaStreamContent.value,
|
||||
},
|
||||
};
|
||||
// Toggle the chat panel within the incident detail page
|
||||
showAIChat.value = !showAIChat.value;
|
||||
};
|
||||
|
||||
// Close the drawer first
|
||||
close();
|
||||
|
||||
// Then open the SRE chat
|
||||
store.dispatch("setIsSREChatOpen", true);
|
||||
const closeAIChat = () => {
|
||||
showAIChat.value = false;
|
||||
};
|
||||
|
||||
const getTimezone = () => {
|
||||
@@ -1653,6 +1728,7 @@ export default defineComponent({
|
||||
hasCorrelatedData,
|
||||
hasAnyStreams,
|
||||
telemetryTimeRange,
|
||||
incidentContextData,
|
||||
refreshCorrelation,
|
||||
close,
|
||||
acknowledgeIncident,
|
||||
@@ -1660,6 +1736,8 @@ export default defineComponent({
|
||||
reopenIncident,
|
||||
triggerRca,
|
||||
openSREChat,
|
||||
closeAIChat,
|
||||
showAIChat,
|
||||
scrollToSection,
|
||||
toggleSection,
|
||||
getStatusColor,
|
||||
@@ -1786,6 +1864,29 @@ body.body--dark .incident-details-column::-webkit-scrollbar-thumb,
|
||||
body.body--dark .tabs-content-column .tw:overflow-auto::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
/* AI Chat Panel Styles */
|
||||
.ai-chat-panel {
|
||||
min-width: 380px;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.ai-chat-panel::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.ai-chat-panel::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ai-chat-panel::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
body.body--dark .ai-chat-panel::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -449,26 +449,35 @@ describe("IncidentList.vue", () => {
|
||||
|
||||
it("should set selected incident and navigate when viewing incident", async () => {
|
||||
const incident = wrapper.vm.incidents[0];
|
||||
const pushSpy = vi.spyOn(router, 'push');
|
||||
|
||||
// viewIncident sets selectedIncident and navigates via router
|
||||
// viewIncident navigates via router to incident detail page
|
||||
wrapper.vm.viewIncident(incident);
|
||||
|
||||
// selectedIncident is set synchronously
|
||||
expect(wrapper.vm.selectedIncident).toBe(incident);
|
||||
|
||||
// showDetailDrawer is set after router navigation completes
|
||||
// In tests, we just verify the navigation intent by checking selectedIncident
|
||||
// Verify router.push was called with correct route
|
||||
expect(pushSpy).toHaveBeenCalledWith({
|
||||
name: "incidentDetail",
|
||||
params: {
|
||||
id: incident.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should set correct incident when viewing", async () => {
|
||||
const incident = createIncident({ id: "test-123", title: "Test Incident" });
|
||||
wrapper.vm.incidents.push(incident);
|
||||
const pushSpy = vi.spyOn(router, 'push');
|
||||
|
||||
await wrapper.vm.viewIncident(incident);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.vm.selectedIncident.id).toBe("test-123");
|
||||
expect(wrapper.vm.selectedIncident.title).toBe("Test Incident");
|
||||
// Verify router.push was called with correct incident ID
|
||||
expect(pushSpy).toHaveBeenCalledWith({
|
||||
name: "incidentDetail",
|
||||
params: {
|
||||
id: "test-123",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -561,20 +570,6 @@ describe("IncidentList.vue", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status Updated Event Handler", () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = createWrapper();
|
||||
await flushPromises();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should reload incidents when status is updated from drawer", async () => {
|
||||
await wrapper.vm.onStatusUpdated();
|
||||
|
||||
expect(incidentsService.list).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Utility Functions - Status Colors", () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
|
||||
@@ -15,10 +15,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div data-test="incident-list" class="tw:w-full tw:h-full tw:pl-[0.625rem] tw:pb-[0.625rem]">
|
||||
<!-- Incidents table -->
|
||||
<div class="tw:w-full tw:h-full tw:pb-[0.625rem]">
|
||||
<div class="card-container tw:h-[calc(100vh-120px)]">
|
||||
<div data-test="incident-list" class="flex q-mt-xs">
|
||||
<div class="tw:w-full tw:h-full tw:px-[0.625rem] tw:pb-[0.625rem]">
|
||||
<!-- Header with title and search -->
|
||||
<div class="card-container tw:mb-[0.625rem]">
|
||||
<div class="tw:flex tw:justify-between tw:items-center tw:w-full tw:py-3 tw:px-4 tw:h-[68px]">
|
||||
<div class="q-table__title tw:font-[600]" data-test="incidents-list-title">
|
||||
{{ t("alerts.incidents.title") }}
|
||||
</div>
|
||||
|
||||
<div class="tw:flex tw:items-center">
|
||||
<q-input
|
||||
v-model="searchQuery"
|
||||
dense
|
||||
borderless
|
||||
:placeholder="t('alerts.incidents.search')"
|
||||
data-test="incident-search-input"
|
||||
clearable
|
||||
class="o2-search-input"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon class="o2-search-input-icon" name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Incidents table -->
|
||||
<div class="tw:w-full tw:h-full tw:pb-[0.625rem]">
|
||||
<div class="card-container tw:h-[calc(100vh-128px)]">
|
||||
<q-table
|
||||
ref="qTableRef"
|
||||
v-model:pagination="pagination"
|
||||
@@ -168,19 +193,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Incident detail drawer -->
|
||||
<q-dialog
|
||||
v-model="showDetailDrawer"
|
||||
position="right"
|
||||
full-height
|
||||
:maximized="true"
|
||||
>
|
||||
<IncidentDetailDrawer
|
||||
@close="closeDrawer"
|
||||
@status-updated="onStatusUpdated"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -192,7 +205,6 @@ import { useQuasar } from "quasar";
|
||||
import { useRouter } from "vue-router";
|
||||
import { date } from "quasar";
|
||||
import incidentsService, { Incident } from "@/services/incidents";
|
||||
import IncidentDetailDrawer from "./IncidentDetailDrawer.vue";
|
||||
import QTablePagination from "@/components/shared/grid/Pagination.vue";
|
||||
import O2AIContextAddBtn from "@/components/common/O2AIContextAddBtn.vue";
|
||||
import NoData from "../shared/grid/NoData.vue";
|
||||
@@ -200,18 +212,11 @@ import NoData from "../shared/grid/NoData.vue";
|
||||
export default defineComponent({
|
||||
name: "IncidentList",
|
||||
components: {
|
||||
IncidentDetailDrawer,
|
||||
QTablePagination,
|
||||
O2AIContextAddBtn,
|
||||
NoData,
|
||||
},
|
||||
props: {
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const $q = useQuasar();
|
||||
@@ -221,8 +226,7 @@ export default defineComponent({
|
||||
const loading = ref(false);
|
||||
const incidents = ref<Incident[]>([]);
|
||||
const allIncidents = ref<Incident[]>([]); // Store all incidents for FE filtering
|
||||
const showDetailDrawer = ref(false);
|
||||
const selectedIncident = ref<Incident | null>(null); // Keep for reference but not passed to drawer
|
||||
const searchQuery = ref("");
|
||||
|
||||
// Filter state for status and severity columns
|
||||
const statusFilter = ref<string[]>([]);
|
||||
@@ -375,7 +379,7 @@ export default defineComponent({
|
||||
const limit = 1000; // Large limit for FE filtering
|
||||
const offset = 0;
|
||||
// Keep keyword parameter for future BE implementation
|
||||
const keyword = undefined; // props.searchQuery?.trim() || undefined;
|
||||
const keyword = undefined; // searchQuery.value?.trim() || undefined;
|
||||
|
||||
const response = await incidentsService.list(
|
||||
org,
|
||||
@@ -389,7 +393,7 @@ export default defineComponent({
|
||||
allIncidents.value = response.data.incidents;
|
||||
|
||||
// Apply frontend search filter
|
||||
const filteredIncidents = applyFrontendSearch(allIncidents.value, props.searchQuery);
|
||||
const filteredIncidents = applyFrontendSearch(allIncidents.value, searchQuery.value);
|
||||
|
||||
// Apply pagination on filtered results
|
||||
const startIndex = (pagination.value.page - 1) * pagination.value.rowsPerPage;
|
||||
@@ -414,7 +418,7 @@ export default defineComponent({
|
||||
pagination.value.descending = props.pagination.descending;
|
||||
|
||||
// For FE filtering, just reapply filters and pagination without API call
|
||||
const filteredIncidents = applyFrontendSearch(allIncidents.value, props.searchQuery);
|
||||
const filteredIncidents = applyFrontendSearch(allIncidents.value, searchQuery.value);
|
||||
const startIndex = (pagination.value.page - 1) * pagination.value.rowsPerPage;
|
||||
const endIndex = startIndex + pagination.value.rowsPerPage;
|
||||
incidents.value = filteredIncidents.slice(startIndex, endIndex);
|
||||
@@ -422,17 +426,12 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const viewIncident = (incident: Incident) => {
|
||||
selectedIncident.value = incident;
|
||||
|
||||
// Add incident ID to URL and open drawer after navigation completes
|
||||
// Navigate to incident detail page
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
incident_id: incident.id,
|
||||
name: "incidentDetail",
|
||||
params: {
|
||||
id: incident.id,
|
||||
},
|
||||
}).then(() => {
|
||||
// Open drawer after URL is updated
|
||||
showDetailDrawer.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -466,20 +465,6 @@ export default defineComponent({
|
||||
updateStatus(incident, "open");
|
||||
};
|
||||
|
||||
const onStatusUpdated = () => {
|
||||
loadIncidents();
|
||||
};
|
||||
|
||||
const closeDrawer = () => {
|
||||
// Close drawer immediately for better UX
|
||||
showDetailDrawer.value = false;
|
||||
selectedIncident.value = null;
|
||||
|
||||
// Remove incident_id from URL
|
||||
const query = { ...router.currentRoute.value.query };
|
||||
delete query.incident_id;
|
||||
router.replace({ query }); // Use replace instead of push to not add history entry
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -538,39 +523,21 @@ export default defineComponent({
|
||||
|
||||
onMounted(async () => {
|
||||
await loadIncidents();
|
||||
|
||||
// Check if there's an incident_id in the URL and open the drawer
|
||||
const incidentIdFromUrl = router.currentRoute.value.query.incident_id as string;
|
||||
if (incidentIdFromUrl) {
|
||||
// Just open the drawer - the IncidentDetailDrawer will read the ID from URL
|
||||
showDetailDrawer.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for search query changes and apply FE filter
|
||||
watch(() => props.searchQuery, () => {
|
||||
watch(() => searchQuery.value, () => {
|
||||
// Reset to page 1 when search query changes
|
||||
pagination.value.page = 1;
|
||||
|
||||
// Apply FE search filter without API call
|
||||
const filteredIncidents = applyFrontendSearch(allIncidents.value, props.searchQuery);
|
||||
const filteredIncidents = applyFrontendSearch(allIncidents.value, searchQuery.value);
|
||||
const startIndex = 0; // Always start from first page
|
||||
const endIndex = pagination.value.rowsPerPage;
|
||||
incidents.value = filteredIncidents.slice(startIndex, endIndex);
|
||||
pagination.value.rowsNumber = filteredIncidents.length;
|
||||
});
|
||||
|
||||
// Watch for drawer closing (handles ESC key, clicking outside, etc.)
|
||||
watch(showDetailDrawer, (isOpen) => {
|
||||
if (!isOpen && router.currentRoute.value.query.incident_id) {
|
||||
// Drawer was closed but incident_id is still in URL - clean it up
|
||||
const query = { ...router.currentRoute.value.query };
|
||||
delete query.incident_id;
|
||||
router.replace({ query });
|
||||
selectedIncident.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Filter toggle functions
|
||||
const toggleStatusFilter = (status: string) => {
|
||||
const index = statusFilter.value.indexOf(status);
|
||||
@@ -632,16 +599,13 @@ export default defineComponent({
|
||||
severityOptions,
|
||||
pagination,
|
||||
columns,
|
||||
showDetailDrawer,
|
||||
selectedIncident,
|
||||
searchQuery,
|
||||
loadIncidents,
|
||||
onRequest,
|
||||
viewIncident,
|
||||
closeDrawer,
|
||||
acknowledgeIncident,
|
||||
resolveIncident,
|
||||
reopenIncident,
|
||||
onStatusUpdated,
|
||||
getStatusColor,
|
||||
getStatusLabel,
|
||||
getSeverityColor,
|
||||
@@ -667,4 +631,8 @@ export default defineComponent({
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.o2-search-input {
|
||||
width: 250px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="tw:w-full tw:h-full tw:px-[0.625rem] tw:py-[0.625rem]" style="display: flex; flex-direction: column;">
|
||||
<div class="tw:w-full tw:px-[0.625rem] tw:py-[0.625rem] card-container" style="display: flex; flex-direction: column; height: calc(100vh - 52px);">
|
||||
|
||||
<!-- 1st section - Streams container -->
|
||||
<div class="feature-card"
|
||||
|
||||
@@ -39,6 +39,7 @@ const LogStream = () => import("@/views/LogStream.vue");
|
||||
const Alerts = () => import("@/views/AppAlerts.vue");
|
||||
const Dashboards = () => import("@/views/Dashboards/Dashboards.vue");
|
||||
const AlertList = () => import("@/components/alerts/AlertList.vue");
|
||||
const IncidentList = () => import("@/components/alerts/IncidentList.vue");
|
||||
const Settings = () => import("@/components/settings/index.vue");
|
||||
|
||||
const Functions = () => import("@/views/Functions.vue");
|
||||
@@ -413,6 +414,28 @@ const useRoutes = () => {
|
||||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "incidents",
|
||||
name: "incidentList",
|
||||
component: IncidentList,
|
||||
meta: {
|
||||
title: "Incidents",
|
||||
},
|
||||
beforeEnter(to: any, from: any, next: any) {
|
||||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "incidents/:id",
|
||||
name: "incidentDetail",
|
||||
component: () => import("@/components/alerts/IncidentDetailDrawer.vue"),
|
||||
meta: {
|
||||
title: "Incident Detail",
|
||||
},
|
||||
beforeEnter(to: any, from: any, next: any) {
|
||||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "short/:id",
|
||||
name: "shortUrl",
|
||||
|
||||
@@ -213,6 +213,7 @@ import {
|
||||
outlinedDescription,
|
||||
outlinedCode,
|
||||
outlinedDevices,
|
||||
outlinedNotificationsActive,
|
||||
} from "@quasar/extras/material-icons-outlined";
|
||||
import SlackIcon from "@/components/icons/SlackIcon.vue";
|
||||
import ManagementIcon from "@/components/icons/ManagementIcon.vue";
|
||||
@@ -435,6 +436,12 @@ export default defineComponent({
|
||||
link: "/alerts",
|
||||
name: "alertList",
|
||||
},
|
||||
{
|
||||
title: t("menu.incidents"),
|
||||
icon: outlinedNotificationsActive,
|
||||
link: "/incidents",
|
||||
name: "incidentList",
|
||||
},
|
||||
{
|
||||
title: t("menu.ingestion"),
|
||||
icon: outlinedFilterAlt,
|
||||
@@ -557,16 +564,16 @@ export default defineComponent({
|
||||
|
||||
const updateActionsMenu = () => {
|
||||
if (isActionsEnabled.value) {
|
||||
const alertIndex = linksList.value.findIndex(
|
||||
(link) => link.name === "alertList",
|
||||
const incidentIndex = linksList.value.findIndex(
|
||||
(link) => link.name === "incidentList",
|
||||
);
|
||||
|
||||
const actionExists = linksList.value.some(
|
||||
(link) => link.name === "actionScripts",
|
||||
);
|
||||
|
||||
if (alertIndex !== -1 && !actionExists) {
|
||||
linksList.value.splice(alertIndex + 1, 0, {
|
||||
if (incidentIndex !== -1 && !actionExists) {
|
||||
linksList.value.splice(incidentIndex + 1, 0, {
|
||||
title: t("menu.actions"),
|
||||
icon: outlinedCode,
|
||||
link: "/actions",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "Berichte",
|
||||
"pipeline": "Rohrleitungen",
|
||||
"language": "Sprache",
|
||||
"actions": "Aktionen"
|
||||
"actions": "Aktionen",
|
||||
"incidents": "Vorfälle"
|
||||
},
|
||||
"rum": {
|
||||
"title": "Echte Benutzerüberwachung",
|
||||
|
||||
@@ -324,6 +324,7 @@
|
||||
"help": "Help",
|
||||
"dashboard": "Dashboards",
|
||||
"alerts": "Alerts",
|
||||
"incidents": "Incidents",
|
||||
"openapi": "OpenAPI",
|
||||
"docs": "Docs",
|
||||
"billings": "Billing",
|
||||
@@ -1738,6 +1739,7 @@
|
||||
"incidents": {
|
||||
"title": "Incidents",
|
||||
"header": "Alert Incidents",
|
||||
"search": "Search incidents...",
|
||||
"noIncidents": "No incidents found",
|
||||
"id": "ID",
|
||||
"status": "Status",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "Informes",
|
||||
"pipeline": "Tuberías",
|
||||
"language": "Idioma",
|
||||
"actions": "Acciones"
|
||||
"actions": "Acciones",
|
||||
"incidents": "Incidentes"
|
||||
},
|
||||
"rum": {
|
||||
"title": "Monitorización de usuarios reales",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "Rapports",
|
||||
"pipeline": "Canalisations",
|
||||
"language": "Langue",
|
||||
"actions": "Actions"
|
||||
"actions": "Actions",
|
||||
"incidents": "Incidents"
|
||||
},
|
||||
"rum": {
|
||||
"title": "Surveillance réelle des utilisateurs",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "Rapporti",
|
||||
"pipeline": "Oleodotti",
|
||||
"language": "Lingua",
|
||||
"actions": "Azioni"
|
||||
"actions": "Azioni",
|
||||
"incidents": "Incidenti"
|
||||
},
|
||||
"rum": {
|
||||
"title": "Monitoraggio degli utenti reali",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "レポート",
|
||||
"pipeline": "パイプライン",
|
||||
"language": "言語",
|
||||
"actions": "アクション"
|
||||
"actions": "アクション",
|
||||
"incidents": "インシデント"
|
||||
},
|
||||
"rum": {
|
||||
"title": "リアルユーザーモニタリング",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "보고서",
|
||||
"pipeline": "파이프라인",
|
||||
"language": "언어",
|
||||
"actions": "액션"
|
||||
"actions": "액션",
|
||||
"incidents": "인시던트"
|
||||
},
|
||||
"rum": {
|
||||
"title": "실제 사용자 모니터링",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "Rapporten",
|
||||
"pipeline": "Pijpleidingen",
|
||||
"language": "taal",
|
||||
"actions": "Acties"
|
||||
"actions": "Acties",
|
||||
"incidents": "Incidenten"
|
||||
},
|
||||
"rum": {
|
||||
"title": "Monitoring van echte gebruikers",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "Relatórios",
|
||||
"pipeline": "Oleodutos",
|
||||
"language": "Idioma",
|
||||
"actions": "Ações"
|
||||
"actions": "Ações",
|
||||
"incidents": "Incidentes"
|
||||
},
|
||||
"rum": {
|
||||
"title": "Monitoramento de usuários reais",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "Raporlar",
|
||||
"pipeline": "Boru hatları",
|
||||
"language": "Dil",
|
||||
"actions": "Eylemler"
|
||||
"actions": "Eylemler",
|
||||
"incidents": "Olaylar"
|
||||
},
|
||||
"rum": {
|
||||
"title": "Gerçek Kullanıcı İzleme",
|
||||
|
||||
@@ -334,7 +334,8 @@
|
||||
"report": "报表",
|
||||
"pipeline": "流水线",
|
||||
"language": "语言",
|
||||
"actions": "操作"
|
||||
"actions": "操作",
|
||||
"incidents": "事件"
|
||||
},
|
||||
"rum": {
|
||||
"title": "用户体验监控",
|
||||
|
||||
@@ -529,12 +529,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
|
||||
<!-- Tab Panels (no tabs in embedded mode, controlled by parent) -->
|
||||
<q-tab-panels
|
||||
<q-card
|
||||
v-model="activeTab"
|
||||
animated
|
||||
class="correlation-content tw:flex-1 tw:overflow-auto"
|
||||
>
|
||||
<q-tab-panel name="logs" class="tw:p-0">
|
||||
<div v-if="activeTab == 'logs'" class="tw:p-0">
|
||||
<!-- Refresh Button -->
|
||||
<div v-if="logsDashboardData" class="tw:p-2 tw:border-b tw:border-solid tw:border-[var(--o2-border-color)] tw:flex tw:justify-end">
|
||||
<q-btn
|
||||
@@ -549,6 +549,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<RenderDashboardCharts
|
||||
v-if="logsDashboardData"
|
||||
:key="logsDashboardRenderKey"
|
||||
@@ -558,46 +559,48 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
:allowAlertCreation="false"
|
||||
searchType="dashboards"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="activeTab == 'metrics'" class="tw:h-full">
|
||||
<div class=" tw:p-2 tw:border-b tw:border-solid tw:border-[var(--o2-border-color)] tw:flex tw:items-center tw:justify-end tw:gap-2">
|
||||
<q-btn
|
||||
v-if="dashboardData"
|
||||
flat
|
||||
dense
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
:label="t('common.refresh')"
|
||||
@click="loadDashboard"
|
||||
:loading="loading"
|
||||
size="sm"
|
||||
/>
|
||||
<q-btn
|
||||
outline
|
||||
dense
|
||||
no-caps
|
||||
color="primary"
|
||||
icon="show_chart"
|
||||
:label="`${selectedMetricStreams.length} of ${uniqueMetricStreams.length} Metric(s)`"
|
||||
@click="showMetricSelector = true"
|
||||
>
|
||||
<q-tooltip>{{ t('correlation.metricsTooltip') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="tw:p-0 tw:flex-1 tw:overflow-auto" style="height: calc(100vh - 272px);">
|
||||
<RenderDashboardCharts
|
||||
v-if="dashboardData"
|
||||
:key="dashboardRenderKey"
|
||||
:dashboardData="dashboardData"
|
||||
:currentTimeObj="currentTimeObj"
|
||||
:viewOnly="true"
|
||||
:allowAlertCreation="false"
|
||||
searchType="dashboards"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-tab-panel name="metrics" class="tw:p-0">
|
||||
<div class="tw:p-3 tw:border-b tw:border-solid tw:border-[var(--o2-border-color)] tw:flex tw:items-center tw:justify-end tw:gap-2">
|
||||
<q-btn
|
||||
v-if="dashboardData"
|
||||
flat
|
||||
dense
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
:label="t('common.refresh')"
|
||||
@click="loadDashboard"
|
||||
:loading="loading"
|
||||
size="sm"
|
||||
/>
|
||||
<q-btn
|
||||
outline
|
||||
dense
|
||||
no-caps
|
||||
color="primary"
|
||||
icon="show_chart"
|
||||
:label="`${selectedMetricStreams.length} of ${uniqueMetricStreams.length} Metric(s)`"
|
||||
@click="showMetricSelector = true"
|
||||
>
|
||||
<q-tooltip>{{ t('correlation.metricsTooltip') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<RenderDashboardCharts
|
||||
v-if="dashboardData"
|
||||
:key="dashboardRenderKey"
|
||||
:dashboardData="dashboardData"
|
||||
:currentTimeObj="currentTimeObj"
|
||||
:viewOnly="true"
|
||||
:allowAlertCreation="false"
|
||||
searchType="dashboards"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="traces" class="tw:p-0">
|
||||
<div v-if="activeTab == 'traces'" class="tw:h-full">
|
||||
<!-- Refresh Button -->
|
||||
<div v-if="traceCorrelationMode !== null" class="tw:p-2 tw:border-b tw:border-solid tw:border-[var(--o2-border-color)] tw:flex tw:justify-end">
|
||||
<q-btn
|
||||
@@ -615,7 +618,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="tracesLoading"
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:h-full tw:py-20"
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:h-[calc(100vh-272px)] tw:py-20"
|
||||
>
|
||||
<q-spinner-hourglass color="primary" size="3.75rem" class="tw:mb-4" />
|
||||
<div class="tw:text-base">{{ t('correlation.loadingTraces') }}</div>
|
||||
@@ -728,7 +731,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
|
||||
<!-- Traces Table -->
|
||||
<div class="tw:p-3 tw:overflow-auto" style="max-height: calc(100% - 4rem)">
|
||||
<div class="tw:p-3 tw:overflow-auto" style="max-height: calc(100vh - 332px)">
|
||||
<q-table
|
||||
:rows="tracesForDimensions"
|
||||
:columns="traceListColumns"
|
||||
@@ -816,8 +819,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
{{ t('correlation.correlatedTracesFor', { service: serviceName }) }}
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Metric Stream Selector Dialog -->
|
||||
@@ -896,6 +899,7 @@ import { SELECT_ALL_VALUE } from "@/utils/dashboard/constants";
|
||||
import streamService from "@/services/stream";
|
||||
import searchService from "@/services/search";
|
||||
import { b64EncodeUnicode } from "@/utils/zincutils";
|
||||
import LogstashDatasource from "@/components/ingestion/logs/LogstashDatasource.vue";
|
||||
|
||||
const RenderDashboardCharts = defineAsyncComponent(
|
||||
() => import("@/views/Dashboards/RenderDashboardCharts.vue")
|
||||
|
||||
Reference in New Issue
Block a user