mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
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>
</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>
</td>
</tr>
<tr>
<td>
<details>
<summary><strong>PatternStatistics.vue</strong><dd><code>Simplify
PatternStatistics to summary text</code>
</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>
</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>
</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>
</td>
</tr>
<tr>
<td>
<details>
<summary><strong>PatternList.vue</strong><dd><code>Polish PatternList
header layout</code>
</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>
</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:
co-authored by
greptile-apps[bot]
github-actions[bot]
parent
cdc49125a3
commit
1d05e20a87
@@ -119,7 +119,7 @@ export const useHistogram = () => {
|
||||
|
||||
if (searchObj.meta.logsVisualizeToggle === "logs") {
|
||||
title +=
|
||||
"(" +
|
||||
" (" +
|
||||
scanSizeLabel +
|
||||
": " +
|
||||
formatSizeFromMB(searchObj.data.queryResults.scan_size) +
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -276,7 +276,8 @@
|
||||
"actionLabel": "アクション",
|
||||
"transformLabel": "トランスフォーム",
|
||||
"addToDashboard": "ダッシュボードに追加",
|
||||
"selectStream": "[ストリーム] を選択"
|
||||
"selectStream": "[ストリーム] を選択",
|
||||
"pattern_summary": "{totalTime} ミリ秒で {logsAnalyzed} イベントで見つかった {totalEvents} イベントと {PatternsFound} パターンのうち 1 ~ 50 件を表示しています。"
|
||||
},
|
||||
"menu": {
|
||||
"home": "ホーム",
|
||||
|
||||
@@ -276,7 +276,8 @@
|
||||
"actionLabel": "액션",
|
||||
"transformLabel": "트랜스폼",
|
||||
"addToDashboard": "대시보드에 추가",
|
||||
"selectStream": "스트림 선택"
|
||||
"selectStream": "스트림 선택",
|
||||
"pattern_summary": "{TotalTime} 밀리초 내에 {logsAnallets} 이벤트에서 발견된 {TotalEvents} 이벤트 및 {PatternsFound} 패턴 중 1~50개를 표시합니다."
|
||||
},
|
||||
"menu": {
|
||||
"home": "홈",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -276,7 +276,8 @@
|
||||
"actionLabel": "行动",
|
||||
"transformLabel": "转型",
|
||||
"addToDashboard": "添加到控制面板",
|
||||
"selectStream": "选择直播"
|
||||
"selectStream": "选择直播",
|
||||
"pattern_summary": "在 {totalTime} 毫秒内显示在 {LogsAnalyzed} 事件中找到的 {TotalEvents} 事件和 {PatternsFound} 模式中的 1 到 50 个。"
|
||||
},
|
||||
"menu": {
|
||||
"home": "主页",
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user