fix: patterns ui fixes (#9663)

### **PR Type**
Enhancement


___

### **Description**
- Add dynamic pattern summary text in SearchResult

- Refactor PatternStatistics into computed summary text

- Update UI styling for PatternCard and PatternList

- Improve constant extraction and filtering logic


___

### Diagram Walkthrough


```mermaid
flowchart LR
  SR["SearchResult Component"]
  PS["PatternStatistics Refactor"]
  PC["PatternCard Styling"]
  PL["PatternList Header Styling"]
  SR -- "compute `patternSummaryText`" --> PS
  SR -- "toggle patterns/logs view" --> SR
  PS -- "provide `summaryText`" --> SR
  SR -- "render pattern list/cards" --> PL & PC
```



<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><table>
<tr>
  <td>
    <details>
<summary><strong>SearchResult.vue</strong><dd><code>Add dynamic pattern
summary in SearchResult</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

web/src/plugins/logs/SearchResult.vue

<ul><li>Add <code>patternSummaryText</code> reactive ref<br> <li>
Conditional pattern summary toggle in UI<br> <li> Enhance constant
extraction with regex split<br> <li> Escape special chars in
<code>match_all</code> clauses</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9663/files#diff-5e65533aa4754bb6d0074fda18ffc1ecacded3b9ca6bc28a161c3f6b12f20195">+40/-17</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>PatternStatistics.vue</strong><dd><code>Simplify
PatternStatistics to summary text</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

web/src/plugins/logs/patterns/PatternStatistics.vue

<ul><li>Replace detailed cards with single summary div<br> <li>
Introduce <code>summaryText</code> computed property<br> <li> Accept
<code>totalEvents</code> and <code>histogramTime</code> props</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9663/files#diff-55950ab8a9ba093fa7462081b33dc7f4af8eb91192e9ad8b642d5474769c6741">+23/-95</a>&nbsp;
</td>

</tr>
</table></td></tr><tr><td><strong>Formatting</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>PatternCard.vue</strong><dd><code>Refine PatternCard
styling classes</code>&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/plugins/logs/patterns/PatternCard.vue

<ul><li>Update padding classes (<code>tw-px-2</code>) on row and
columns<br> <li> Replace custom border & hover classes<br> <li> Remove
fixed monospace font size constraint</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9663/files#diff-5ee84ac6fd187a905532b25f0fb3f9fc0640804fed81c924eeafb9f33a384bb6">+8/-8</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>PatternList.vue</strong><dd><code>Polish PatternList
header layout</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; </dd></summary>
<hr>

web/src/plugins/logs/patterns/PatternList.vue

<ul><li>Simplify header class names and spacing<br> <li> Use CSS var for
header background<br> <li> Add <code>table-head</code> and
<code>table-row-hover</code> utilities</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9663/files#diff-e2daac67b374ee564fa289bbd99f16a7523749e7e830e45fe8f343ae43be0be6">+12/-14</a>&nbsp;
</td>

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

</details>

___

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Ashish Kolhe
2025-12-18 10:56:32 +05:30
committed by GitHub
co-authored by greptile-apps[bot] github-actions[bot]
parent cdc49125a3
commit 1d05e20a87
17 changed files with 158 additions and 197 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ export const useHistogram = () => {
if (searchObj.meta.logsVisualizeToggle === "logs") {
title +=
"(" +
" (" +
scanSizeLabel +
": " +
formatSizeFromMB(searchObj.data.queryResults.scan_size) +
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Aktion",
"transformLabel": "Verwandeln",
"addToDashboard": "Zum Dashboard hinzufügen",
"selectStream": "Wählen Sie Stream"
"selectStream": "Wählen Sie Stream",
"pattern_summary": "Es werden 1 bis 50 von {totalEvents} -Ereignissen und {patternsFound} -Mustern angezeigt, die in {logsAnalyzed} -Ereignissen in {totalTime} ms gefunden wurden."
},
"menu": {
"home": "Startseite",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Action",
"transformLabel": "Transform",
"addToDashboard": "Add To Dashboard",
"selectStream": "Select Stream"
"selectStream": "Select Stream",
"pattern_summary": "Showing 1 to 50 out of {totalEvents} events & {patternsFound} patterns found in {logsAnalyzed} events in {totalTime} ms."
},
"menu": {
"home": "Home",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Acción",
"transformLabel": "Transformar",
"addToDashboard": "Agregar al panel",
"selectStream": "Selecciona Stream"
"selectStream": "Selecciona Stream",
"pattern_summary": "Se muestran de 1 a 50 de los eventos y {patternsFound} patrones de {totalEvents} encontrados en eventos de {logsAnalyzed} en {totalTime} ms."
},
"menu": {
"home": "Inicio",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Action",
"transformLabel": "Transformez",
"addToDashboard": "Ajouter au tableau de bord",
"selectStream": "Sélectionnez Stream"
"selectStream": "Sélectionnez Stream",
"pattern_summary": "Affichage de 1 à 50 événements sur {TotalEvents} et des modèles {PatternsFound} trouvés dans les événements {LogsAnalyzed} en {TotalTime} ms."
},
"menu": {
"home": "Accueil",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Azione",
"transformLabel": "Trasforma",
"addToDashboard": "Aggiungi al pannello di controllo",
"selectStream": "Seleziona Stream"
"selectStream": "Seleziona Stream",
"pattern_summary": "Mostrando da 1 a 50 eventi su {totalEvents} e {patternsFound} pattern trovati in {logsAnalyzed} eventi in {totalTime} ms."
},
"menu": {
"home": "Home",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "アクション",
"transformLabel": "トランスフォーム",
"addToDashboard": "ダッシュボードに追加",
"selectStream": "[ストリーム] を選択"
"selectStream": "[ストリーム] を選択",
"pattern_summary": "{totalTime} ミリ秒で {logsAnalyzed} イベントで見つかった {totalEvents} イベントと {PatternsFound} パターンのうち 1 ~ 50 件を表示しています。"
},
"menu": {
"home": "ホーム",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "액션",
"transformLabel": "트랜스폼",
"addToDashboard": "대시보드에 추가",
"selectStream": "스트림 선택"
"selectStream": "스트림 선택",
"pattern_summary": "{TotalTime} 밀리초 내에 {logsAnallets} 이벤트에서 발견된 {TotalEvents} 이벤트 및 {PatternsFound} 패턴 중 1~50개를 표시합니다."
},
"menu": {
"home": "홈",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Actie",
"transformLabel": "Transformeren",
"addToDashboard": "Aan dashboard toevoegen",
"selectStream": "Selecteer Stream"
"selectStream": "Selecteer Stream",
"pattern_summary": "Toont 1 tot 50 van de {totalEvents} -gebeurtenissen en {PatternsFound} -patronen gevonden in {logsAnalyzed} -gebeurtenissen in {totalTime} ms."
},
"menu": {
"home": "Home",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Ação",
"transformLabel": "Transformar",
"addToDashboard": "Adicionar ao painel",
"selectStream": "Selecione Stream"
"selectStream": "Selecione Stream",
"pattern_summary": "Mostrando de 1 a 50 dos eventos {totalEvents} e padrões {patternsFound} encontrados em eventos {logsAnalyzed} em {totalTime} ms."
},
"menu": {
"home": "Início",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "Eylem",
"transformLabel": "Dönüştürün",
"addToDashboard": "Kontrol Paneline Ekle",
"selectStream": "Akışı Seç"
"selectStream": "Akışı Seç",
"pattern_summary": "{totalTime} ms içinde {logsAnalyzed} olaylarında bulunan {totalEvents} olay ve {patternsFound} modelinden 1 ila 50 arası gösteriliyor."
},
"menu": {
"home": "Ana Sayfa",
+2 -1
View File
@@ -276,7 +276,8 @@
"actionLabel": "行动",
"transformLabel": "转型",
"addToDashboard": "添加到控制面板",
"selectStream": "选择直播"
"selectStream": "选择直播",
"pattern_summary": "在 {totalTime} 毫秒内显示在 {LogsAnalyzed} 事件中找到的 {TotalEvents} 事件和 {PatternsFound} 模式中的 1 到 50 个。"
},
"menu": {
"home": "主页",
+53 -17
View File
@@ -38,7 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</div>
<div v-else class="col-7 text-left q-pl-lg warning flex items-center">
{{ noOfRecordsTitle }}
{{ searchObj.meta.logsVisualizeToggle === 'patterns' ? patternSummaryText : noOfRecordsTitle }}
<span v-if="searchObj.loadingCounter" class="q-ml-md">
<q-spinner-hourglass
color="primary"
@@ -311,12 +311,6 @@ color="warning" size="xs"></q-icon> Error while
: 'table-container--with-histogram',
]"
>
<!-- Statistics Bar -->
<PatternStatistics
v-if="patternsState?.patterns?.statistics"
:statistics="patternsState?.patterns?.statistics"
/>
<!-- Patterns List -->
<PatternList
:patterns="patternsState?.patterns?.patterns || []"
@@ -471,9 +465,6 @@ export default defineComponent({
EqualIcon,
NotEqualIcon,
TelemetryCorrelationDashboard,
PatternStatistics: defineAsyncComponent(
() => import("./patterns/PatternStatistics.vue"),
),
PatternList: defineAsyncComponent(
() => import("./patterns/PatternList.vue"),
),
@@ -501,6 +492,22 @@ export default defineComponent({
},
},
methods: {
formatPatternSummary(stats: any, totalEvents: number, histogramMs: number) {
const patternsFound = stats?.total_patterns_found || 0;
const logsAnalyzed = (stats?.total_logs_analyzed || 0).toLocaleString();
const totalEventsStr = totalEvents ? totalEvents.toLocaleString() : logsAnalyzed;
// Combine histogram time + pattern extraction time
const patternMs = stats?.extraction_time_ms || 0;
const totalTimeMs = histogramMs + patternMs;
return this.$t("search.pattern_summary", {
totalEvents: totalEventsStr,
patternsFound: patternsFound,
logsAnalyzed: logsAnalyzed,
totalTime: totalTimeMs
});
},
handleColumnSizesUpdate(newColSizes: any) {
const prevColSizes =
this.searchObj.data.resultGrid?.colSizes[
@@ -689,6 +696,7 @@ export default defineComponent({
const $q = useQuasar();
const searchListContainer = ref(null);
const noOfRecordsTitle = ref("");
const patternSummaryText = ref("");
const scrollPosition = ref(0);
const rowsPerPageOptions = [10, 25, 50, 100];
const disableMoreErrorDetails = ref(false);
@@ -1054,14 +1062,16 @@ export default defineComponent({
const extractConstantsFromPattern = (template: string): string[] => {
// Extract longest non-variable strings from pattern template
// Pattern template has format like: "INFO action <*> at 14:47.1755283"
// We want continuous strings between <*> that are longer than 10 chars
// Variables can be: <*>, <:IDENTIFIERS>, <:TIMESTAMP>, <:UNIX_TIMESTAMP>, etc.
// We want continuous strings between these variables that are longer than 10 chars
const constants: string[] = [];
const parts = template.split("<*>");
// Split by all variable markers using regex
// Matches: <*>, <:WORD>, etc.
const parts = template.split(/<[*:][^>]*>/);
for (const part of parts) {
const trimmed = part.trim();
// For now, use the string as-is without sanitization
// const sanitized = sanitizeForMatchAll(trimmed);
// Only include strings longer than 10 characters
if (trimmed.length > 10) {
constants.push(trimmed);
@@ -1090,10 +1100,15 @@ export default defineComponent({
// Build multiple match_all() clauses, one for each constant
// Each match_all takes a single string
const matchAllClauses = constants.map((constant) => {
// Escape backslashes first, then single quotes in the constant
// Escape special characters for match_all query
// Order matters: backslash must be escaped first
const escapedConstant = constant
.replace(/\\/g, "\\\\")
.replace(/'/g, "\\'");
.replace(/\\/g, "\\\\") // Escape backslashes
.replace(/'/g, "\\'") // Escape single quotes
.replace(/"/g, '\\"') // Escape double quotes
.replace(/\n/g, "\\n") // Escape newlines
.replace(/\r/g, "\\r") // Escape carriage returns
.replace(/\t/g, "\\t"); // Escape tabs
return `match_all('${escapedConstant}')`;
});
@@ -1111,6 +1126,9 @@ export default defineComponent({
// Set the filter to be added to the query
searchObj.data.stream.addToFilter = filterExpression;
// Switch to logs view to show the filtered results
searchObj.meta.logsVisualizeToggle = "logs";
};
const getRowIndex = (next: boolean, prev: boolean, oldIndex: number) => {
@@ -1374,6 +1392,7 @@ export default defineComponent({
extractFTSFields,
useLocalWrapContent,
noOfRecordsTitle,
patternSummaryText,
scrollPosition,
rowsPerPageOptions,
pageNumberInput,
@@ -1429,6 +1448,9 @@ export default defineComponent({
updateTitle() {
return this.searchObj.data.histogram.chartParams.title;
},
updatePatternSummary() {
return this.patternsState?.patterns?.statistics;
},
reDrawChartData() {
return this.searchObj.data.histogram;
},
@@ -1451,6 +1473,20 @@ export default defineComponent({
updateTitle() {
this.noOfRecordsTitle = this.searchObj.data.histogram.chartParams.title;
},
updatePatternSummary() {
if (this.patternsState?.patterns?.statistics) {
// Reuse the same summary logic from PatternStatistics component
const stats = this.patternsState.patterns.statistics;
const totalEvents = this.searchObj.data.queryResults?.total || stats.total_logs_analyzed || 0;
const histogramMs = this.searchObj.data.queryResults?.took || 0;
this.patternSummaryText = this.formatPatternSummary(
stats,
totalEvents,
histogramMs
);
}
},
reDrawChartData: {
deep: true,
handler: function () {
@@ -16,14 +16,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div
class="tw-flex tw-items-center tw-border-b tw-border-[var(--o2-border-color)] tw-cursor-pointer hover:tw-bg-[var(--o2-hover-gray)] tw-py-2 tw-px-3"
class="tw-flex tw-items-center tw-border-b tw-cursor-pointer hover:tw-bg-[var(--o2-hover-gray)] table-row-hover"
@click="$emit('click', pattern, index)"
:data-test="`pattern-card-${index}`"
>
<!-- Pattern Column -->
<div class="tw-flex-1 tw-min-w-0 tw-pr-1">
<div class="tw-flex-1 tw-min-w-0 tw-px-2">
<div
class="tw-text-xs tw-truncate o2-monospace-font"
class="tw-truncate o2-monospace-font"
:class="store.state.theme === 'dark' ? 'text-grey-4' : 'text-grey-8'"
:data-test="`pattern-card-${index}-template`"
:title="pattern.template"
@@ -39,9 +39,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<!-- Occurrence Column -->
<div class="tw-w-16 tw-flex-shrink-0 tw-pr-1 tw-text-right">
<div class="tw-w-16 tw-flex-shrink-0 tw-px-2 tw-text-right">
<span
class="tw-text-xs text-weight-bold text-primary"
class="text-weight-bold text-primary"
:data-test="`pattern-card-${index}-frequency`"
>
{{ pattern.frequency.toLocaleString() }}
@@ -49,16 +49,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<!-- Percentage Column -->
<div class="tw-w-14 tw-flex-shrink-0 tw-pr-1 tw-text-right">
<div class="tw-w-14 tw-flex-shrink-0 tw-px-2 tw-text-right">
<span
class="tw-text-xs text-weight-bold text-primary"
class="text-weight-bold text-primary"
:data-test="`pattern-card-${index}-percentage`"
>{{ pattern.percentage.toFixed(2) }}%</span
>
</div>
<!-- Actions Column -->
<div class="tw-w-20 tw-flex-shrink-0 tw-flex tw-items-center">
<div class="tw-w-20 tw-flex-shrink-0 tw-px-2 tw-flex tw-items-center">
<q-btn
size="6px"
@click.stop="$emit('include', pattern)"
+12 -14
View File
@@ -20,43 +20,41 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div v-if="patterns?.length > 0" class="tw-flex tw-flex-col tw-h-full">
<!-- Table Header -->
<div
class="tw-flex tw-items-center tw-py-2 tw-px-3 tw-border-b tw-border-[var(--o2-border-color)] tw-sticky tw-top-0 tw-z-10"
:style="{
background: store.state.theme === 'dark' ? '#565656' : '#E0E0E0',
}"
class="tw-flex tw-items-center tw-border-b tw-border-[var(--o2-border-color)] tw-sticky tw-top-0 tw-z-10"
style="background: var(--o2-table-header-bg); min-width: 100%;"
>
<!-- Pattern Column Header -->
<div class="tw-flex-1 tw-min-w-0 tw-pr-1">
<div class="tw-flex-1 tw-min-w-0 tw-px-2 tw-relative table-head tw-text-ellipsis tw-text-left">
<span
class="tw-text-xs text-weight-bold"
:class="store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'"
class="tw-font-bold"
:class="store.state.theme === 'dark' ? 'text-white' : 'text-grey-8'"
>
{{ t("search.patternColumnHeader") }}
</span>
</div>
<!-- Occurrence Column Header -->
<div class="tw-w-16 tw-flex-shrink-0 tw-pr-1 tw-text-right">
<div class="tw-w-16 tw-flex-shrink-0 tw-px-2 tw-relative table-head tw-text-ellipsis tw-text-right">
<span
class="tw-text-xs text-weight-bold"
:class="store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'"
class="tw-font-bold"
:class="store.state.theme === 'dark' ? 'text-white' : 'text-grey-8'"
>
{{ t("search.occurrenceColumnHeader") }}
</span>
</div>
<!-- Percentage Column Header -->
<div class="tw-w-14 tw-flex-shrink-0 tw-pr-1 tw-text-right">
<div class="tw-w-14 tw-flex-shrink-0 tw-px-2 tw-relative table-head tw-text-ellipsis tw-text-right">
<span
class="tw-text-xs text-weight-bold"
:class="store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'"
class="tw-font-bold"
:class="store.state.theme === 'dark' ? 'text-white' : 'text-grey-8'"
>
{{ t("search.percentageColumnHeader") }}
</span>
</div>
<!-- Actions Column - No Header -->
<div class="tw-w-20 tw-flex-shrink-0">
<div class="tw-w-20 tw-flex-shrink-0 tw-px-2 tw-relative table-head">
</div>
</div>
@@ -51,77 +51,65 @@ describe("PatternStatistics", () => {
).toBe(true);
});
describe("Statistics Cards", () => {
it("should display logs scanned card with correct value", () => {
const logsScannedCard = wrapper.find(
'[data-test="pattern-stats-logs-scanned-card"]',
);
expect(logsScannedCard.exists()).toBe(true);
describe("Summary Text", () => {
it("should display summary with correct statistics", () => {
const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
const summaryText = summaryElement.text();
const logsScannedValue = wrapper.find(
'[data-test="pattern-stats-logs-scanned-value"]',
);
expect(logsScannedValue.text()).toBe("1,000");
// Check that summary contains the key statistics
expect(summaryText).toContain("1,000"); // logs analyzed
expect(summaryText).toContain("42"); // patterns found
expect(summaryText).toContain("events");
expect(summaryText).toContain("patterns found");
expect(summaryText).toContain("ms");
});
it("should display patterns found card with correct value", () => {
const patternsFoundCard = wrapper.find(
'[data-test="pattern-stats-patterns-found-card"]',
);
expect(patternsFoundCard.exists()).toBe(true);
it("should display correct format", () => {
const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
const summaryText = summaryElement.text();
const patternsFoundValue = wrapper.find(
'[data-test="pattern-stats-patterns-found-value"]',
);
expect(patternsFoundValue.text()).toBe("42");
// Check the full format
expect(summaryText).toMatch(/Showing 1 to 50 out of \d+/);
expect(summaryText).toMatch(/\d+ patterns found in \d+/);
});
it("should display coverage card with correct value", () => {
const coverageCard = wrapper.find(
'[data-test="pattern-stats-coverage-card"]',
);
expect(coverageCard.exists()).toBe(true);
it("should handle totalEvents prop", async () => {
await wrapper.setProps({
statistics: mockStatistics,
totalEvents: 5000,
});
const coverageValue = wrapper.find(
'[data-test="pattern-stats-coverage-value"]',
);
expect(coverageValue.text()).toBe("87.5%");
const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
const summaryText = summaryElement.text();
expect(summaryText).toContain("5,000"); // totalEvents
});
it("should display processing time card with correct value", () => {
const processingTimeCard = wrapper.find(
'[data-test="pattern-stats-processing-time-card"]',
);
expect(processingTimeCard.exists()).toBe(true);
it("should combine histogram time with extraction time", async () => {
await wrapper.setProps({
statistics: mockStatistics,
histogramTime: 50,
});
const processingTimeValue = wrapper.find(
'[data-test="pattern-stats-processing-time-value"]',
);
expect(processingTimeValue.text()).toBe("150ms");
const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
const summaryText = summaryElement.text();
// Should show 50 (histogram) + 150 (extraction) = 200ms
expect(summaryText).toContain("200 ms");
});
});
describe("Empty Statistics", () => {
it("should display zero values when statistics are null", async () => {
await wrapper.setProps({
statistics: null,
});
const patternsFoundValue = wrapper.find(
'[data-test="pattern-stats-patterns-found-value"]',
);
expect(patternsFoundValue.text()).toBe("0");
const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
const summaryText = summaryElement.text();
const coverageValue = wrapper.find(
'[data-test="pattern-stats-coverage-value"]',
);
expect(coverageValue.text()).toBe("0.0%");
const processingTimeValue = wrapper.find(
'[data-test="pattern-stats-processing-time-value"]',
);
expect(processingTimeValue.text()).toBe("0ms");
expect(summaryText).toContain("0 patterns found");
expect(summaryText).toContain("0 events");
});
});
});
@@ -15,111 +15,39 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="tw-py-[0.375rem] tw-shrink-0 tw-bg-[var(--o2-bg-gray)]" data-test="pattern-statistics">
<div class="row q-col-gutter-md !tw-mx-[0.375rem]">
<!-- Total Logs Scanned Card -->
<div class="col-3 !tw-pl-0">
<q-card
flat
class="tw-bg-[var(--o2-card-bg)]"
data-test="pattern-stats-logs-scanned-card"
>
<q-card-section class="tw-py-[0.375rem] tw-px-[0.625rem]">
<div
class="text-caption"
:class="
store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'
"
>
Total Logs Scanned
</div>
<div class="text-h5 text-weight-bold q-mt-xs text-primary" data-test="pattern-stats-logs-scanned-value">
{{ (statistics?.total_logs_analyzed || 0).toLocaleString() }}
</div>
</q-card-section>
</q-card>
</div>
<!-- Patterns Found Card -->
<div class="col-3 !tw-pl-[0.375rem]">
<q-card
flat
class="tw-bg-[var(--o2-card-bg)]"
data-test="pattern-stats-patterns-found-card"
>
<q-card-section class="tw-py-[0.375rem] tw-px-[0.625rem]">
<div
class="text-caption"
:class="
store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'
"
>
Patterns Found
</div>
<div class="text-h5 text-weight-bold q-mt-xs text-primary" data-test="pattern-stats-patterns-found-value">
{{ statistics?.total_patterns_found || 0 }}
</div>
</q-card-section>
</q-card>
</div>
<!-- Coverage Card -->
<div class="col-3 !tw-pl-[0.375rem]">
<q-card
flat
class="tw-bg-[var(--o2-card-bg)]"
data-test="pattern-stats-coverage-card"
>
<q-card-section class="tw-py-[0.375rem] tw-px-[0.625rem]">
<div
class="text-caption"
:class="
store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'
"
>
Coverage
</div>
<div class="text-h5 text-weight-bold q-mt-xs text-primary" data-test="pattern-stats-coverage-value">
{{ (statistics?.coverage_percentage || 0).toFixed(1) }}%
</div>
</q-card-section>
</q-card>
</div>
<!-- Processing Time Card -->
<div class="col-3 !tw-pl-[0.375rem]">
<q-card
flat
class="tw-bg-[var(--o2-card-bg)]"
data-test="pattern-stats-processing-time-card"
>
<q-card-section class="tw-py-[0.375rem] tw-px-[0.625rem]">
<div
class="text-caption"
:class="
store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'
"
>
Processing Time
</div>
<div class="text-h5 text-weight-bold q-mt-xs text-primary" data-test="pattern-stats-processing-time-value">
{{ statistics?.extraction_time_ms || 0 }}ms
</div>
</q-card-section>
</q-card>
</div>
</div>
<div
class="tw-py-2 tw-px-4 tw-shrink-0 tw-bg-[var(--o2-bg-gray)] tw-text-sm"
data-test="pattern-statistics"
:class="store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'"
>
{{ summaryText }}
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useStore } from "vuex";
defineProps<{
const props = defineProps<{
statistics: any;
totalEvents?: number;
histogramTime?: number;
}>();
const store = useStore();
const summaryText = computed(() => {
const patternsFound = props.statistics?.total_patterns_found || 0;
const logsAnalyzed = (props.statistics?.total_logs_analyzed || 0).toLocaleString();
const totalEvents = props.totalEvents ? props.totalEvents.toLocaleString() : logsAnalyzed;
// Combine histogram time + pattern extraction time
const histogramMs = props.histogramTime || 0;
const patternMs = props.statistics?.extraction_time_ms || 0;
const totalTimeMs = histogramMs + patternMs;
return `Showing 1 to 50 out of ${totalEvents} events & ${patternsFound} patterns found in ${logsAnalyzed} events in ${totalTimeMs} ms.`;
});
</script>
<style scoped lang="scss">