mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 00:08:55 +00:00
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:
co-authored by
Shrinath Rao
Claude Opus 4.6
parent
32665d50f5
commit
65666e4ed9
Generated
+15
@@ -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",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"googleapis": "^171.4.0",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"uuid": "^14.0.0",
|
||||
"winston": "^3.11.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user