chore: last refreshed for logs and traces (#11398)

<img width="2658" height="420" alt="image"
src="https://github.com/user-attachments/assets/90d7bd1a-012d-4185-a832-35135d696baf"
/>
<img width="2640" height="486" alt="image"
src="https://github.com/user-attachments/assets/d9543813-1cca-4f83-8da6-a12b7514a986"
/>

---------

Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashish Kolhe
2026-04-23 11:30:00 +05:30
committed by GitHub
co-authored by Shrinath Rao Claude Opus 4.6
parent 32665d50f5
commit 65666e4ed9
18 changed files with 714 additions and 3 deletions
+15
View File
@@ -15,6 +15,7 @@
"googleapis": "^171.4.0",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"uuid": "^14.0.0",
"winston": "^3.11.0"
},
"devDependencies": {
@@ -251,6 +252,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -1276,6 +1278,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+2 -1
View File
@@ -24,6 +24,7 @@
"googleapis": "^171.4.0",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"uuid": "^14.0.0",
"winston": "^3.11.0"
}
}
}
+167
View File
@@ -0,0 +1,167 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import OButton from './Button.vue'
describe('OButton', () => {
// --- Slots ---
it('renders default slot content', () => {
const wrapper = mount(OButton, { slots: { default: 'Save' } })
expect(wrapper.text()).toBe('Save')
})
it('renders icon-left slot', () => {
const wrapper = mount(OButton, {
slots: { 'icon-left': '<span data-testid="icon-left">←</span>' },
})
expect(wrapper.find('[data-testid="icon-left"]').exists()).toBe(true)
})
it('renders icon-right slot', () => {
const wrapper = mount(OButton, {
slots: { 'icon-right': '<span data-testid="icon-right">→</span>' },
})
expect(wrapper.find('[data-testid="icon-right"]').exists()).toBe(true)
})
// --- Props ---
it('defaults to type="button"', () => {
const wrapper = mount(OButton)
expect(wrapper.attributes('type')).toBe('button')
})
it('forwards the type prop to the native button', () => {
const wrapper = mount(OButton, { props: { type: 'submit' } })
expect(wrapper.attributes('type')).toBe('submit')
})
it('sets the disabled attribute when disabled prop is true', () => {
const wrapper = mount(OButton, { props: { disabled: true } })
expect(wrapper.attributes('disabled')).toBeDefined()
})
it('sets the disabled attribute when loading prop is true', () => {
const wrapper = mount(OButton, { props: { loading: true } })
expect(wrapper.attributes('disabled')).toBeDefined()
})
// --- Emits ---
it('emits click with the MouseEvent when clicked', async () => {
const wrapper = mount(OButton)
await wrapper.trigger('click')
const emitted = wrapper.emitted('click')
expect(emitted).toHaveLength(1)
expect(emitted![0][0]).toBeInstanceOf(MouseEvent)
})
it('does not emit click when disabled', async () => {
const wrapper = mount(OButton, { props: { disabled: true } })
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeUndefined()
})
it('does not emit click when loading', async () => {
const wrapper = mount(OButton, { props: { loading: true } })
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeUndefined()
})
// --- ARIA ---
it('sets aria-disabled="true" when disabled', () => {
const wrapper = mount(OButton, { props: { disabled: true } })
expect(wrapper.attributes('aria-disabled')).toBe('true')
})
it('sets aria-disabled="true" when loading', () => {
const wrapper = mount(OButton, { props: { loading: true } })
expect(wrapper.attributes('aria-disabled')).toBe('true')
})
it('sets aria-busy="true" when loading', () => {
const wrapper = mount(OButton, { props: { loading: true } })
expect(wrapper.attributes('aria-busy')).toBe('true')
})
it('does not set aria-busy when not loading', () => {
const wrapper = mount(OButton)
expect(wrapper.attributes('aria-busy')).toBeUndefined()
})
it('does not set aria-disabled when not disabled or loading', () => {
const wrapper = mount(OButton)
expect(wrapper.attributes('aria-disabled')).toBeUndefined()
})
// --- Variant classes ---
it('applies primary variant classes by default', () => {
const wrapper = mount(OButton)
expect(wrapper.classes().join(' ')).toContain('tw:bg-button-primary')
})
it('applies secondary variant classes', () => {
const wrapper = mount(OButton, { props: { variant: 'secondary' } })
expect(wrapper.classes().join(' ')).toContain('tw:bg-button-secondary')
})
it('applies outline variant classes', () => {
const wrapper = mount(OButton, { props: { variant: 'outline' } })
expect(wrapper.classes().join(' ')).toContain('tw:text-button-outline-text')
})
it('applies ghost variant classes', () => {
const wrapper = mount(OButton, { props: { variant: 'ghost' } })
expect(wrapper.classes().join(' ')).toContain('tw:text-button-ghost-text')
})
it('applies destructive variant classes', () => {
const wrapper = mount(OButton, { props: { variant: 'destructive' } })
expect(wrapper.classes().join(' ')).toContain('tw:bg-button-destructive')
})
// --- Size classes ---
it('applies md size classes by default', () => {
const wrapper = mount(OButton)
expect(wrapper.classes().join(' ')).toContain('tw:h-10')
})
it('applies sm size classes', () => {
const wrapper = mount(OButton, { props: { size: 'sm' } })
expect(wrapper.classes().join(' ')).toContain('tw:h-8')
})
it('applies lg size classes', () => {
const wrapper = mount(OButton, { props: { size: 'lg' } })
expect(wrapper.classes().join(' ')).toContain('tw:h-12')
})
it('applies icon size classes', () => {
const wrapper = mount(OButton, { props: { size: 'icon' } })
expect(wrapper.classes().join(' ')).toContain('tw:size-9')
})
// --- Keyboard ---
it('emits click on Enter key (native button behaviour)', async () => {
const wrapper = mount(OButton)
await wrapper.trigger('keydown', { key: 'Enter' })
// Native button fires click on Enter — we trust browser behaviour;
// ensure the component does not suppress it when enabled.
// Trigger click directly to confirm handler works.
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
// --- Attrs passthrough ---
it('passes extra attributes to the native button', () => {
const wrapper = mount(OButton, {
attrs: { 'data-testid': 'my-btn' },
})
expect(wrapper.attributes('data-testid')).toBe('my-btn')
})
})
+36
View File
@@ -0,0 +1,36 @@
/**
* Button.types.ts — single source of truth for all OButton public types.
* No types should be defined inline in Button.vue.
*/
/** Visual style variant — design is baked in, no style override props */
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
/** Size controls height, padding, and font-size only */
export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'
export interface ButtonProps {
/** Visual style variant */
variant?: ButtonVariant
/** Component size */
size?: ButtonSize
/** Disables the button and all interaction */
disabled?: boolean
/** Shows loading state and disables interaction */
loading?: boolean
/** Native button type attribute */
type?: 'button' | 'submit' | 'reset'
}
export interface ButtonEmits {
(e: 'click', event: MouseEvent): void
}
export interface ButtonSlots {
/** Main label / content */
default?: () => unknown
/** Icon placed before the label */
'icon-left'?: () => unknown
/** Icon placed after the label */
'icon-right'?: () => unknown
}
+94
View File
@@ -0,0 +1,94 @@
<script setup lang="ts">
import type { ButtonProps, ButtonEmits, ButtonSlots } from './Button.types'
import { computed } from 'vue'
const props = withDefaults(defineProps<ButtonProps>(), {
variant: 'primary',
size: 'md',
type: 'button',
disabled: false,
loading: false,
})
const emit = defineEmits<ButtonEmits>()
defineSlots<ButtonSlots>()
// Variant class map — every entry is a full token-based class string
const variantClasses: Record<NonNullable<ButtonProps['variant']>, string> = {
primary: [
'tw:bg-button-primary tw:text-button-primary-foreground',
'tw:enabled:hover:bg-button-primary-hover',
'tw:enabled:active:bg-button-primary-active',
'tw:focus-visible:ring-4 tw:focus-visible:ring-button-primary-focus-ring',
'tw:disabled:bg-button-primary-disabled tw:disabled:text-button-primary-foreground',
].join(' '),
secondary: [
'tw:bg-button-secondary tw:text-button-secondary-foreground',
'tw:enabled:hover:bg-button-secondary-hover',
'tw:enabled:active:bg-button-secondary-active',
'tw:focus-visible:ring-4 tw:focus-visible:ring-button-secondary-focus-ring',
'tw:disabled:bg-button-secondary-disabled tw:disabled:text-text-disabled',
].join(' '),
outline: [
'tw:bg-transparent tw:text-button-outline-text tw:border tw:border-button-outline-border',
'tw:enabled:hover:bg-button-outline-hover-bg tw:enabled:hover:border-button-outline-hover-border',
'tw:enabled:active:bg-button-outline-active-bg',
'tw:focus-visible:ring-4 tw:focus-visible:ring-button-outline-focus-ring',
'tw:disabled:opacity-50',
].join(' '),
ghost: [
'tw:bg-transparent tw:text-button-ghost-text tw:border-0',
'tw:enabled:hover:bg-button-ghost-hover-bg',
'tw:enabled:active:bg-button-ghost-active-bg',
'tw:focus-visible:ring-4 tw:focus-visible:ring-button-ghost-focus-ring',
'tw:disabled:text-text-disabled',
].join(' '),
destructive: [
'tw:bg-button-destructive tw:text-button-destructive-foreground',
'tw:enabled:hover:bg-button-destructive-hover',
'tw:enabled:active:bg-button-destructive-hover',
'tw:focus-visible:ring-4 tw:focus-visible:ring-error-700',
'tw:disabled:bg-button-destructive-disabled',
].join(' '),
}
const sizeClasses: Record<NonNullable<ButtonProps['size']>, string> = {
sm: 'tw:h-8 tw:ps-3 tw:pe-3 tw:text-sm tw:gap-1.5 tw:rounded-md',
md: 'tw:h-10 tw:ps-4 tw:pe-4 tw:text-sm tw:gap-2 tw:rounded-lg',
lg: 'tw:h-12 tw:ps-6 tw:pe-6 tw:text-base tw:gap-3 tw:rounded-lg',
icon: 'tw:size-9 tw:p-0 tw:rounded-lg',
}
const classes = computed<string[]>(() => [
// Base — layout, typography, interaction
'tw:inline-flex tw:items-center tw:justify-center tw:whitespace-nowrap',
'tw:font-medium tw:transition-colors tw:duration-150',
'tw:outline-none tw:focus-visible:outline-none',
'tw:disabled:cursor-not-allowed tw:enabled:cursor-pointer',
// Variant + size
variantClasses[props.variant],
sizeClasses[props.size],
])
function handleClick(event: MouseEvent): void {
if (props.disabled || props.loading) return
emit('click', event)
}
</script>
<template>
<button
:class="classes"
:type="type"
:disabled="disabled || loading"
:aria-disabled="disabled || loading || undefined"
:aria-busy="loading || undefined"
v-bind="$attrs"
@click="handleClick"
>
<slot name="icon-left" />
<slot />
<slot name="icon-right" />
</button>
</template>
@@ -0,0 +1,12 @@
export interface RefreshButtonProps {
/** Unix millisecond timestamp of the last completed query */
lastRunAt?: number | null
/** Mirrors the page's loading state to spin the icon and disable the button */
loading?: boolean
/** Disables the button independently of loading */
disabled?: boolean
}
export interface RefreshButtonEmits {
(e: 'click', event: MouseEvent): void
}
@@ -0,0 +1,121 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import OButton from '../Button/Button.vue'
import type { RefreshButtonProps, RefreshButtonEmits } from './RefreshButton.types'
const props = withDefaults(defineProps<RefreshButtonProps>(), {
lastRunAt: null,
loading: false,
disabled: false,
})
const emit = defineEmits<RefreshButtonEmits>()
const relativeTime = ref('')
let intervalId: ReturnType<typeof setInterval> | null = null
const diffSeconds = (): number => {
if (!props.lastRunAt) return Infinity
return Math.floor((Date.now() - props.lastRunAt) / 1000)
}
const getRelativeTime = (ts: number): string => {
const sec = Math.floor((Date.now() - ts) / 1000)
if (sec < 5) return 'just now'
if (sec < 60) return `${sec}s ago`
const min = Math.floor(sec / 60)
if (min < 60) return `${min}m ago`
return `${Math.floor(min / 60)}h ago`
}
const updateRelativeTime = () => {
if (!props.lastRunAt) { relativeTime.value = ''; return }
relativeTime.value = getRelativeTime(props.lastRunAt)
}
// green < 30s, amber 30s–5min, red > 5min
const dotColor = computed(() => {
if (props.loading) return 'tw:bg-[var(--color-text-disabled)]'
const s = diffSeconds()
if (s === Infinity) return 'tw:bg-[var(--color-text-disabled)]'
if (s < 30) return 'tw:bg-[var(--color-success-500)]'
if (s < 300) return 'tw:bg-[var(--color-warning-500)]'
return 'tw:bg-[var(--color-error-500)]'
})
const dotTitle = computed(() => {
const s = diffSeconds()
if (s === Infinity) return 'Not yet refreshed'
if (s < 30) return 'Data is fresh'
if (s < 300) return 'Data is getting stale'
return 'Data is stale'
})
const exactTime = computed(() => {
if (!props.lastRunAt) return 'Not yet refreshed'
return `Last refreshed: ${new Date(props.lastRunAt).toLocaleTimeString()}`
})
onMounted(() => {
updateRelativeTime()
intervalId = setInterval(updateRelativeTime, 10_000)
})
onBeforeUnmount(() => {
if (intervalId) clearInterval(intervalId)
})
watch(() => props.lastRunAt, updateRelativeTime, { immediate: true })
function handleClick(e: MouseEvent) {
if (props.loading || props.disabled) return
emit('click', e)
}
</script>
<template>
<div class="tw:inline-flex tw:items-center tw:gap-1.5">
<!-- staleness dot -->
<span
:class="['tw:size-2 tw:rounded-full tw:shrink-0 tw:transition-colors tw:duration-700', dotColor]"
:title="dotTitle"
/>
<!-- relative timestamp -->
<span
v-if="lastRunAt"
class="tw:text-xs tw:text-[var(--color-text-secondary)] tw:tabular-nums tw:whitespace-nowrap tw:select-none"
:title="exactTime"
>
{{ relativeTime || 'just now' }}
</span>
<!-- refresh icon button -->
<OButton
variant="ghost"
size="icon"
:loading="loading"
:disabled="disabled"
:title="exactTime"
data-test="refresh-button"
class="tw:size-[1.75rem]!"
@click="handleClick"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
:class="{ 'tw:animate-spin': loading }"
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M8 16H3v5" />
</svg>
</OButton>
</div>
</template>
+11
View File
@@ -0,0 +1,11 @@
/* ============================================================
O2 Component Library — CSS entry point
Import this file once in the app to activate all tokens.
============================================================ */
@import "tailwindcss" prefix(tw);
@import "./tokens/base.css";
@import "./tokens/semantic.css";
@import "./tokens/component.css";
@import "./tokens/dark.css";
+67
View File
@@ -0,0 +1,67 @@
/* ============================================================
BASE TOKENS — raw palette, spacing, radius, shadows
These are NEVER referenced directly in components.
Components use semantic or component tokens only.
============================================================ */
:root {
/* --- Primary palette --- */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;
/* --- Grey palette --- */
--color-grey-0: #ffffff;
--color-grey-50: #f9fafb;
--color-grey-100: #f3f4f6;
--color-grey-200: #e5e7eb;
--color-grey-300: #d1d5db;
--color-grey-400: #9ca3af;
--color-grey-500: #6b7280;
--color-grey-600: #4b5563;
--color-grey-700: #374151;
--color-grey-800: #1f2937;
--color-grey-900: #111827;
--color-grey-950: #030712;
/* --- Error palette --- */
--color-error-50: #fef2f2;
--color-error-100: #fee2e2;
--color-error-400: #f87171;
--color-error-500: #ef4444;
--color-error-600: #dc2626;
--color-error-700: #b91c1c;
/* --- Success palette --- */
--color-success-50: #f0fdf4;
--color-success-500: #22c55e;
--color-success-700: #15803d;
/* --- Warning palette --- */
--color-warning-50: #fffbeb;
--color-warning-500: #f59e0b;
--color-warning-700: #b45309;
/* --- White / Black --- */
--color-white: #ffffff;
--color-black: #000000;
/* --- Radius scale --- */
--radius-sm: 0.25rem; /* 4px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-xl: 0.75rem; /* 12px */
--radius-full: 9999px;
/* --- Shadow scale --- */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
+44
View File
@@ -0,0 +1,44 @@
/* ============================================================
COMPONENT TOKENS — per-component design decisions
Each section is owned exclusively by one component.
============================================================ */
@theme inline {
/* ---- Button ------------------------------------------------ */
/* primary */
--color-button-primary: var(--color-primary-600);
--color-button-primary-hover: var(--color-primary-700);
--color-button-primary-active: var(--color-primary-800);
--color-button-primary-disabled: var(--color-primary-300);
--color-button-primary-foreground: var(--color-white);
--color-button-primary-focus-ring: var(--color-primary-400);
/* secondary */
--color-button-secondary: var(--color-grey-100);
--color-button-secondary-hover: var(--color-grey-200);
--color-button-secondary-active: var(--color-grey-300);
--color-button-secondary-disabled: var(--color-grey-100);
--color-button-secondary-foreground: var(--color-grey-800);
--color-button-secondary-focus-ring: var(--color-grey-400);
/* outline */
--color-button-outline-text: var(--color-grey-800);
--color-button-outline-border: var(--color-grey-300);
--color-button-outline-hover-bg: var(--color-grey-50);
--color-button-outline-hover-border: var(--color-grey-400);
--color-button-outline-active-bg: var(--color-grey-100);
--color-button-outline-focus-ring: var(--color-grey-400);
/* ghost */
--color-button-ghost-text: var(--color-grey-700);
--color-button-ghost-hover-bg: var(--color-grey-100);
--color-button-ghost-active-bg: var(--color-grey-200);
--color-button-ghost-focus-ring: var(--color-grey-400);
/* destructive */
--color-button-destructive: var(--color-error-600);
--color-button-destructive-hover: var(--color-error-700);
--color-button-destructive-disabled: var(--color-error-300);
--color-button-destructive-foreground: var(--color-white);
}
+46
View File
@@ -0,0 +1,46 @@
/* ============================================================
DARK MODE TOKEN OVERRIDES
Overrides semantic (and component) tokens for dark themes.
============================================================ */
@custom-variant dark (&:where(.dark, .dark *));
.dark {
:root, & {
/* --- Text --- */
--color-text-primary: var(--color-grey-50);
--color-text-secondary: var(--color-grey-400);
--color-text-disabled: var(--color-grey-600);
--color-text-inverse: var(--color-grey-900);
/* --- Surfaces --- */
--color-surface-base: var(--color-grey-950);
--color-surface-panel: var(--color-grey-900);
--color-surface-overlay: var(--color-grey-800);
/* --- Borders --- */
--color-border-default: var(--color-grey-700);
--color-border-strong: var(--color-grey-500);
/* ---- Button overrides ---- */
/* secondary (darkened surface) */
--color-button-secondary: var(--color-grey-800);
--color-button-secondary-hover: var(--color-grey-700);
--color-button-secondary-active: var(--color-grey-600);
--color-button-secondary-disabled: var(--color-grey-800);
--color-button-secondary-foreground: var(--color-grey-100);
/* outline */
--color-button-outline-text: var(--color-grey-100);
--color-button-outline-border: var(--color-grey-600);
--color-button-outline-hover-bg: var(--color-grey-800);
--color-button-outline-hover-border: var(--color-grey-500);
--color-button-outline-active-bg: var(--color-grey-700);
/* ghost */
--color-button-ghost-text: var(--color-grey-200);
--color-button-ghost-hover-bg: var(--color-grey-800);
--color-button-ghost-active-bg: var(--color-grey-700);
}
}
+52
View File
@@ -0,0 +1,52 @@
/* ============================================================
SEMANTIC TOKENS — maps base palette to intent/meaning
Used directly in components for shared design decisions.
============================================================ */
@theme inline {
/* --- Text --- */
--color-text-primary: var(--color-grey-900);
--color-text-secondary: var(--color-grey-500);
--color-text-disabled: var(--color-grey-400);
--color-text-inverse: var(--color-white);
/* --- Surfaces --- */
--color-surface-base: var(--color-white);
--color-surface-panel: var(--color-grey-50);
--color-surface-overlay: var(--color-white);
/* --- Borders --- */
--color-border-default: var(--color-grey-200);
--color-border-strong: var(--color-grey-400);
/* --- Brand / Primary --- */
--color-primary-50: var(--color-primary-50);
--color-primary-100: var(--color-primary-100);
--color-primary-200: var(--color-primary-200);
--color-primary-300: var(--color-primary-300);
--color-primary-400: var(--color-primary-400);
--color-primary-500: var(--color-primary-500);
--color-primary-600: var(--color-primary-600);
--color-primary-700: var(--color-primary-700);
--color-primary-800: var(--color-primary-800);
--color-primary-900: var(--color-primary-900);
/* --- Error / Danger --- */
--color-error-50: var(--color-error-50);
--color-error-100: var(--color-error-100);
--color-error-400: var(--color-error-400);
--color-error-500: var(--color-error-500);
--color-error-600: var(--color-error-600);
--color-error-700: var(--color-error-700);
/* --- Radius / Shadow (semantic aliases) --- */
--radius-sm: var(--radius-sm);
--radius-md: var(--radius-md);
--radius-lg: var(--radius-lg);
--radius-xl: var(--radius-xl);
--radius-full: var(--radius-full);
--shadow-sm: var(--shadow-sm);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
}
+1
View File
@@ -287,6 +287,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:recordsPerPage="getMoreDataRecordsPerPage"
@expandlog="toggleExpandLog"
@send-to-ai-chat="sendToAiChat"
@run-query="searchData"
/>
</div>
<div class="text-center col-10 q-ma-none">
+19
View File
@@ -123,6 +123,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ t("volumeInsights.analyzeTooltipLogs") }}
</q-tooltip>
</q-btn>
<ORefreshButton
:last-run-at="searchObj.meta.lastRunAt"
:loading="searchObj.loading || searchObj.loadingHistogram"
:disabled="searchObj.loading || searchObj.loadingHistogram"
@click="$emit('run-query')"
class="tw:ml-2"
/>
</div>
<div class="col-4 text-right q-pr-sm q-gutter-xs pagination-block">
@@ -512,10 +519,12 @@ import TelemetryCorrelationDashboard from "@/plugins/correlation/TelemetryCorrel
import type { TelemetryContext } from "@/utils/telemetryCorrelation";
import { useServiceCorrelation } from "@/composables/useServiceCorrelation";
import config from "@/aws-exports";
import ORefreshButton from "@/lib/core/RefreshButton/RefreshButton.vue";
export default defineComponent({
name: "SearchResult",
components: {
ORefreshButton,
DetailTable: defineAsyncComponent(() => import("./DetailTable.vue")),
ChartRenderer: defineAsyncComponent(
() => import("@/components/dashboards/panels/ChartRenderer.vue"),
@@ -545,6 +554,7 @@ export default defineComponent({
"update:recordsPerPage",
"update:columnSizes",
"sendToAiChat",
"run-query",
],
props: {
expandedLogs: {
@@ -1338,6 +1348,15 @@ export default defineComponent({
return searchObj.meta.resetPlotChart;
});
watch(
() => searchObj.loading,
(loading, wasLoading) => {
if (wasLoading && !loading && searchObj.meta.searchApplied) {
searchObj.meta.lastRunAt = Date.now();
}
},
);
watch(resetPlotChart, (newVal) => {
if (newVal) {
plotChart.value = {};
+1
View File
@@ -245,6 +245,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:sort="runQueryOnSort"
@shareLink="copyTracesUrl"
@metrics:filters-updated="onMetricsFiltersUpdated"
@run-query="searchData"
/>
</div>
</div>
@@ -341,6 +341,7 @@ describe("SearchResult", () => {
"search:timeboxed",
"get:traceDetails",
"metrics:filters-updated",
"run-query",
];
expect(wrapper.vm.$options.emits).toEqual(expectedEmits);
+21 -2
View File
@@ -55,12 +55,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
color="primary"
icon="timeline"
:label="t('volumeInsights.insightsButtonLabel')"
class="analyze-button tw:h-[2rem] tw:text-[0.75rem]! tw:tracking-[0.03rem]! tw:font-bold! tw:mr-[0.5rem]"
class="analyze-button tw:h-[2rem] tw:text-[0.75rem]! tw:tracking-[0.03rem]! tw:font-bold!"
@click.stop="openUnifiedAnalysisDashboard"
data-test="insights-button"
>
<q-tooltip>{{ t("volumeInsights.analyzeTooltipTraces") }}</q-tooltip>
</q-btn>
<ORefreshButton
:last-run-at="searchObj.meta.lastRunAt"
:loading="searchObj.loading"
:disabled="searchObj.loading"
@click="$emit('run-query')"
class="tw:ml-2"
/>
<q-space />
<!-- Pagination -->
@@ -159,7 +166,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, ref } from "vue";
import { computed, defineAsyncComponent, defineComponent, ref, watch } from "vue";
import { useQuasar } from "quasar";
import { useStore } from "vuex";
import { useI18n } from "vue-i18n";
@@ -168,10 +175,12 @@ import useTraces from "../../composables/useTraces";
import { useRouter } from "vue-router";
import TracesSearchResultList from "./components/TracesSearchResultList.vue";
import { formatLargeNumber } from "../../utils/zincutils";
import ORefreshButton from "@/lib/core/RefreshButton/RefreshButton.vue";
export default defineComponent({
name: "SearchResult",
components: {
ORefreshButton,
TracesSearchResultList,
TracesMetricsDashboard: defineAsyncComponent(
() => import("./metrics/TracesMetricsDashboard.vue"),
@@ -185,6 +194,7 @@ export default defineComponent({
"search:timeboxed",
"get:traceDetails",
"metrics:filters-updated",
"run-query",
],
methods: {
closeColumn(col: any) {
@@ -215,6 +225,15 @@ export default defineComponent({
const { searchObj, updatedLocalLogFilterField } = useTraces();
const metricsDashboardRef: any = ref(null);
watch(
() => searchObj.loading,
(loading, wasLoading) => {
if (wasLoading && !loading && searchObj.searchApplied) {
searchObj.meta.lastRunAt = Date.now();
}
},
);
const expandRowDetail = (props: any) => {
let from: number;
let to: number;
+4
View File
@@ -1,4 +1,8 @@
@import "tailwindcss" prefix(tw);
@import "../lib/styles/tokens/base.css";
@import "../lib/styles/tokens/semantic.css";
@import "../lib/styles/tokens/component.css";
@import "../lib/styles/tokens/dark.css";
@plugin "@tailwindcss/typography";