mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
fix: added skeleton loading for traces table (#12593)
## Why The custom spinner-based loading states were inconsistent across traces views anddid not align with the logs TenstackTable skeleton pattern already established inthe codebase. The new skeleton provides a unified, low-friction loading experience — shimmer pills at column-aligned positions that mirror real cell widths — and removes the need for each consumer to write its own loading template.
This commit is contained in:
@@ -325,13 +325,28 @@ describe("TenstackTable", () => {
|
||||
|
||||
// ── Loading state ─────────────────────────────────────────────────────────
|
||||
describe("loading state", () => {
|
||||
it("should show the default loading indicator when loading=true and no rows", () => {
|
||||
it("should show the skeleton loading body when loading=true and no rows", () => {
|
||||
wrapper = mountTable({ rows: [], loading: true });
|
||||
expect(
|
||||
wrapper.find('[data-test="tenstack-table-loading-indicator"]').exists(),
|
||||
wrapper.find('[data-test="tenstack-table-skeleton-body"]').exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should not show the skeleton body when a custom loading slot is provided", () => {
|
||||
wrapper = mountTable(
|
||||
{ rows: [], loading: true },
|
||||
{ loading: '<div data-test="custom-loading-slot">Loading…</div>' },
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="tenstack-table-skeleton-body"]').exists(),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should show the skeleton body when loading=true (follows O2 pattern)", () => {
|
||||
wrapper = mountTable({ loading: true });
|
||||
expect(
|
||||
wrapper.find('[data-test="tenstack-table-skeleton-body"]').exists(),
|
||||
).toBe(true);
|
||||
// i18n key is rendered as-is by the mock
|
||||
expect(wrapper.html()).toContain("confirmDialog.loading");
|
||||
});
|
||||
|
||||
it("should render a custom loading slot when loading=true and rows is empty", () => {
|
||||
|
||||
@@ -304,8 +304,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</th>
|
||||
</vue-draggable>
|
||||
|
||||
<!-- Loading row: shown when there are no rows yet (initial fetch) -->
|
||||
<tr v-if="loading && tableRows.length === 0" class="tw:w-full">
|
||||
<!-- Loading row: only rendered when the parent supplies a custom
|
||||
#loading slot. With no slot, the shimmer skeleton <tbody> below
|
||||
takes over (initial fetch). -->
|
||||
<tr
|
||||
v-if="loading && tableRows.length === 0 && $slots.loading"
|
||||
class="tw:w-full"
|
||||
>
|
||||
<td
|
||||
:colspan="columnOrder.length"
|
||||
class="tw:font-bold"
|
||||
@@ -315,18 +320,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
opacity: 0.7,
|
||||
}"
|
||||
>
|
||||
<slot name="loading">
|
||||
<div
|
||||
class="tw:text-sm tw:font-medium text-weight-bold tw:flex tw:items-center"
|
||||
>
|
||||
<OSpinner size="xs" data-test="tenstack-table-loading-indicator" />
|
||||
{{ t("confirmDialog.loading") }}
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="loading" />
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Loading banner: shown above rows while a new page is fetching -->
|
||||
<tr v-if="loading && tableRows.length > 0" class="tw:w-full">
|
||||
<tr v-if="loading && tableRows.length > 0 && $slots['loading-banner']" class="tw:w-full">
|
||||
<td :colspan="columnOrder.length">
|
||||
<slot name="loading-banner" />
|
||||
</td>
|
||||
@@ -392,6 +390,58 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- Skeleton loading body — shimmer placeholder shown on initial fetch
|
||||
when the parent does not provide a custom #loading slot. Mirrors the
|
||||
logs TenstackTable pattern. -->
|
||||
<tbody
|
||||
v-if="loading && !$slots.loading"
|
||||
data-test="tenstack-table-skeleton-body"
|
||||
aria-busy="true"
|
||||
aria-label="Loading"
|
||||
>
|
||||
<!-- Rows use tw:flex to align with the real virtual/data rows. -->
|
||||
<tr
|
||||
v-for="r in SKEL_ROW_COUNT"
|
||||
:key="`skel-${r}`"
|
||||
class="o2-skel-row tw:flex tw:items-center tw:w-full"
|
||||
:style="{ animationDelay: `${(r - 1) * 40}ms` }"
|
||||
>
|
||||
<!-- No columns yet (first paint) — full-width shimmer bar -->
|
||||
<td
|
||||
v-if="!headers?.length"
|
||||
class="tw:w-full tw:px-4 tw:overflow-hidden"
|
||||
>
|
||||
<span
|
||||
class="o2-skel-pill tw:inline-block tw:h-3 tw:rounded-md"
|
||||
:style="{ width: `${skelCellWidth(r - 1, 0)}%` }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</td>
|
||||
<!-- Columns available — per-column aligned shimmer pills -->
|
||||
<template v-else>
|
||||
<td
|
||||
v-for="(header, c) in headers"
|
||||
:key="header.id"
|
||||
class="tw:px-2 tw:overflow-hidden"
|
||||
:class="c === 0 ? 'tw:pl-4' : ''"
|
||||
:style="skelTdStyle(header, c)"
|
||||
>
|
||||
<span
|
||||
class="o2-skel-pill tw:inline-block tw:h-3 tw:rounded-md"
|
||||
:style="{
|
||||
width:
|
||||
c === 0
|
||||
? `${SKEL_TIMESTAMP_PX}px`
|
||||
: `${skelCellWidth(r - 1, c)}%`,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<!-- tw:relative is only needed for virtual-scroll absolute rows (logs/traces).
|
||||
In dashboard mode (regular DOM rows) it must be absent so that
|
||||
position:sticky on <thead> works correctly. -->
|
||||
@@ -1063,7 +1113,6 @@ import {
|
||||
PIVOT_TABLE_ROW_KEY_SEPARATOR,
|
||||
} from "@/utils/dashboard/constants";
|
||||
import JsonFieldRenderer from "@/components/dashboards/panels/JsonFieldRenderer.vue";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
|
||||
export interface StreamField {
|
||||
name: string;
|
||||
@@ -1837,6 +1886,29 @@ const headerGroups = computed(() => table?.getHeaderGroups()[0]);
|
||||
|
||||
const headers = computed(() => headerGroups.value.headers);
|
||||
|
||||
// ── Skeleton loading helpers — mirrors the logs TenstackTable shimmer pattern ──
|
||||
const SKEL_ROW_COUNT = 30;
|
||||
const SKEL_BASE_WIDTHS = [55, 70, 60, 45, 65, 50, 75, 40, 58, 68, 48, 62];
|
||||
const SKEL_JITTER = [0, 6, -4, 3, -2, 5, -3, 2, -5, 4, -1, 6];
|
||||
// First column (timestamp): pill sized to "2026-06-02 12:09:00.349"
|
||||
// (23 chars × 7.2 px/char in monospace 12 px).
|
||||
const SKEL_TIMESTAMP_PX = Math.round("2026-06-02 12:09:00.349".length * 7.2);
|
||||
const skelCellWidth = (r: number, c: number): number => {
|
||||
const base = SKEL_BASE_WIDTHS[c % SKEL_BASE_WIDTHS.length] ?? 60;
|
||||
const jit = SKEL_JITTER[(r + c) % SKEL_JITTER.length] ?? 0;
|
||||
return Math.max(25, Math.min(85, base + jit));
|
||||
};
|
||||
// Mirror the exact width/flex logic of the real cells so skeleton columns align.
|
||||
// Source column (width:'auto' in real rows) → flex:1 to fill remaining space.
|
||||
// All other columns: fixed width from the CSS variable (same as real rows).
|
||||
const skelTdStyle = (header: any, c: number): Record<string, string> => {
|
||||
const colId = header.column.id;
|
||||
const isStretchSource = colId === "source" && !header.column.getCanResize();
|
||||
if (isStretchSource) return { flex: "1 1 0", minWidth: "0" };
|
||||
const w = `calc(var(--col-${sanitizeCssId(colId)}-size) * 1px)`;
|
||||
return { width: w, minWidth: w, flexShrink: "0" };
|
||||
};
|
||||
|
||||
watch(
|
||||
() => headers.value,
|
||||
(newVal) => {
|
||||
@@ -2372,6 +2444,67 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
// ── Loading skeleton ─────────────────────────────────────────────
|
||||
.o2-skel-row {
|
||||
opacity: 0;
|
||||
animation: o2-skel-row-in 320ms ease-out forwards;
|
||||
border-bottom: 1px solid var(--o2-tag-grey-1);
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.o2-skel-pill {
|
||||
// Light mode — grey-100 shimmer (matches logs/histogram skeletons)
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-grey-100) 0%,
|
||||
rgba(255, 255, 255, 0.65) 50%,
|
||||
var(--color-grey-100) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: o2-skel-shimmer 1.5s ease-in-out infinite;
|
||||
|
||||
// Dark mode — grey-600 shimmer
|
||||
.body--dark & {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-grey-600) 0%,
|
||||
rgba(255, 255, 255, 0.03) 50%,
|
||||
var(--color-grey-600) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o2-skel-shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o2-skel-row-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.o2-skel-row {
|
||||
opacity: 1;
|
||||
animation: none;
|
||||
}
|
||||
.o2-skel-pill {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy button — only visible on cell hover, sized to match text so it
|
||||
// doesn't inflate the row height.
|
||||
.copy-cell-td {
|
||||
|
||||
@@ -242,26 +242,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
class="tw:p-0! panel-section tw:mb-0!"
|
||||
data-test="service-graph-side-panel-recent-operations"
|
||||
>
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="loadingOperations"
|
||||
class="tw:flex tw:items-center tw:gap-2 tw:py-3 tw:text-sm"
|
||||
v-if="recentOperations.length === 0 && !loadingOperations"
|
||||
class="tw:text-xs tw:italic tw:py-2 tw:text-center"
|
||||
style="color: var(--o2-text-secondary)"
|
||||
>
|
||||
<OSpinner size="xs" data-test="service-graph-operations-loading-indicator" />
|
||||
<span>Loading operations...</span>
|
||||
No operations found
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="recentOperations.length === 0"
|
||||
class="tw:text-xs tw:italic tw:py-2 tw:text-center"
|
||||
style="color: var(--o2-text-secondary)"
|
||||
>
|
||||
No operations found
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
<div
|
||||
v-else-if="recentOperations.length > 0 || loadingOperations"
|
||||
class="tw:overflow-hidden tw:rounded"
|
||||
data-test="service-graph-side-panel-operations-table"
|
||||
>
|
||||
@@ -270,7 +259,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
:rows="sortedOperationsTableRows"
|
||||
:sort-by="sortBy"
|
||||
:sort-order="sortOrder"
|
||||
:loading="false"
|
||||
:loading="loadingOperations"
|
||||
:default-columns="false"
|
||||
:enable-column-reorder="false"
|
||||
:enable-row-expand="false"
|
||||
@@ -356,8 +345,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
</template>
|
||||
</TenstackTable>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</OTabPanel>
|
||||
|
||||
<!-- Nodes Tab -->
|
||||
@@ -370,121 +358,111 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
:data-test="`service-graph-side-panel-${cfg.id}`"
|
||||
>
|
||||
<div
|
||||
v-if="resourceTabLoading[cfg.id]"
|
||||
class="tw:flex tw:items-center tw:gap-2 tw:py-3 tw:text-sm"
|
||||
v-if="!resourceTabData[cfg.id]?.length && !resourceTabLoading[cfg.id]"
|
||||
class="tw:text-xs tw:italic tw:py-2 tw:text-center"
|
||||
style="color: var(--o2-text-secondary)"
|
||||
>
|
||||
<OSpinner size="xs" data-test="service-graph-resource-loading-indicator" />
|
||||
<span>Loading {{ cfg.label.toLowerCase() }}...</span>
|
||||
No {{ cfg.label.toLowerCase() }} data found
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="!resourceTabData[cfg.id]?.length"
|
||||
class="tw:text-xs tw:italic tw:py-2 tw:text-center"
|
||||
style="color: var(--o2-text-secondary)"
|
||||
<div
|
||||
v-else-if="resourceTabData[cfg.id]?.length > 0 || resourceTabLoading[cfg.id]"
|
||||
class="tw:overflow-hidden tw:rounded"
|
||||
:data-test="`service-graph-side-panel-${cfg.id}-table`"
|
||||
>
|
||||
<TenstackTable
|
||||
:columns="buildEntityTableColumns(cfg.colId, cfg.colLabel)"
|
||||
:rows="sortResourceRows(buildResourceTableRows(cfg))"
|
||||
:sort-by="sortBy"
|
||||
:sort-order="sortOrder"
|
||||
:loading="resourceTabLoading[cfg.id]"
|
||||
:default-columns="false"
|
||||
:enable-column-reorder="false"
|
||||
:enable-row-expand="false"
|
||||
:enable-text-highlight="false"
|
||||
:enable-status-bar="false"
|
||||
:enable-ai-context-button="false"
|
||||
:row-height="28"
|
||||
@sort-change="handleSortChange"
|
||||
@click:data-row="
|
||||
(row: any) =>
|
||||
navigateToTraces({
|
||||
resourceFilter: cfg.fields
|
||||
? { fields: cfg.fields, value: row[cfg.colId] }
|
||||
: { field: cfg.groupField, value: row[cfg.colId] },
|
||||
})
|
||||
"
|
||||
>
|
||||
No {{ cfg.label.toLowerCase() }} data found
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="tw:overflow-hidden tw:rounded"
|
||||
:data-test="`service-graph-side-panel-${cfg.id}-table`"
|
||||
>
|
||||
<TenstackTable
|
||||
:columns="buildEntityTableColumns(cfg.colId, cfg.colLabel)"
|
||||
:rows="sortResourceRows(buildResourceTableRows(cfg))"
|
||||
:sort-by="sortBy"
|
||||
:sort-order="sortOrder"
|
||||
:loading="false"
|
||||
:default-columns="false"
|
||||
:enable-column-reorder="false"
|
||||
:enable-row-expand="false"
|
||||
:enable-text-highlight="false"
|
||||
:enable-status-bar="false"
|
||||
:enable-ai-context-button="false"
|
||||
:row-height="28"
|
||||
@sort-change="handleSortChange"
|
||||
@click:data-row="
|
||||
(row: any) =>
|
||||
<template #cell-actions="{ row, column, active }">
|
||||
<OButton
|
||||
v-if="active"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="tw:ml-1 tw:absolute! tw:right-2!"
|
||||
:data-test="`service-graph-side-panel-${cfg.id}-view-traces-btn`"
|
||||
@click.stop="
|
||||
navigateToTraces({
|
||||
resourceFilter: cfg.fields
|
||||
? { fields: cfg.fields, value: row[cfg.colId] }
|
||||
: { field: cfg.groupField, value: row[cfg.colId] },
|
||||
errorsOnly: column.id === 'errors',
|
||||
minDurationMicros: isDurationColumn(column.id) ? row[column.id] : undefined
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #cell-actions="{ row, column, active }">
|
||||
<OButton
|
||||
v-if="active"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="tw:ml-1 tw:absolute! tw:right-2!"
|
||||
:data-test="`service-graph-side-panel-${cfg.id}-view-traces-btn`"
|
||||
@click.stop="
|
||||
navigateToTraces({
|
||||
resourceFilter: cfg.fields
|
||||
? { fields: cfg.fields, value: row[cfg.colId] }
|
||||
: { field: cfg.groupField, value: row[cfg.colId] },
|
||||
errorsOnly: column.id === 'errors',
|
||||
minDurationMicros: isDurationColumn(column.id) ? row[column.id] : undefined
|
||||
})
|
||||
"
|
||||
>
|
||||
<OIcon name="search" size="xs" />
|
||||
<OTooltip content="View in Traces" />
|
||||
</OButton>
|
||||
</template>
|
||||
<template #cell-errors="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.errors > 0
|
||||
? 'tw:text-[var(--q-negative)] tw:font-semibold'
|
||||
: ''
|
||||
"
|
||||
>{{ item.errors }}</span
|
||||
>
|
||||
</template>
|
||||
<template #cell-p99="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.p99 > 0
|
||||
? 'tw:text-[var(--o2-latency-p99)]'
|
||||
: ''
|
||||
"
|
||||
>{{ formatOperationLatency(item.p99) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #cell-p95="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.p95 > 0
|
||||
? 'tw:text-[var(--o2-latency-p95)]'
|
||||
: ''
|
||||
"
|
||||
>{{ formatOperationLatency(item.p95) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #cell-p75="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.p75 > 0
|
||||
? 'tw:text-[var(--o2-latency-p75)]'
|
||||
: ''
|
||||
"
|
||||
>{{ formatOperationLatency(item.p75) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="tw:text-xs tw:italic tw:py-2 tw:text-center"
|
||||
style="color: var(--o2-text-secondary)"
|
||||
>
|
||||
No {{ cfg.label.toLowerCase() }} data found
|
||||
</div>
|
||||
</template>
|
||||
</TenstackTable>
|
||||
</div>
|
||||
</template>
|
||||
"
|
||||
>
|
||||
<OIcon name="search" size="xs" />
|
||||
<OTooltip content="View in Traces" />
|
||||
</OButton>
|
||||
</template>
|
||||
<template #cell-errors="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.errors > 0
|
||||
? 'tw:text-[var(--q-negative)] tw:font-semibold'
|
||||
: ''
|
||||
"
|
||||
>{{ item.errors }}</span
|
||||
>
|
||||
</template>
|
||||
<template #cell-p99="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.p99 > 0
|
||||
? 'tw:text-[var(--o2-latency-p99)]'
|
||||
: ''
|
||||
"
|
||||
>{{ formatOperationLatency(item.p99) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #cell-p95="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.p95 > 0
|
||||
? 'tw:text-[var(--o2-latency-p95)]'
|
||||
: ''
|
||||
"
|
||||
>{{ formatOperationLatency(item.p95) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #cell-p75="{ item }">
|
||||
<span
|
||||
:class="
|
||||
item.p75 > 0
|
||||
? 'tw:text-[var(--o2-latency-p75)]'
|
||||
: ''
|
||||
"
|
||||
>{{ formatOperationLatency(item.p75) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="tw:text-xs tw:italic tw:py-2 tw:text-center"
|
||||
style="color: var(--o2-text-secondary)"
|
||||
>
|
||||
No {{ cfg.label.toLowerCase() }} data found
|
||||
</div>
|
||||
</template>
|
||||
</TenstackTable>
|
||||
</div>
|
||||
</OTabPanel>
|
||||
|
||||
<!-- Metrics Tab -->
|
||||
|
||||
@@ -327,35 +327,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- Loading banner: shown above rows while a next page is fetching -->
|
||||
<template #loading-banner>
|
||||
<div
|
||||
class="tw:flex tw:flex-nowrap tw:items-center tw:px-2 tw:min-w-max tw:min-h-[3.25rem] tw:bg-[var(--o2-card-bg)] tw:border-b tw:border-[var(--o2-border-2)]!"
|
||||
>
|
||||
<OSpinner size="xs" class="tw:mx-[0.25rem]" />
|
||||
<span
|
||||
class="tw:tracking-[0.03rem] tw:text-[0.85rem] tw:text-[var(--o2-text-1)] tw:font-bold"
|
||||
>
|
||||
{{ t("traces.servicesCatalog.loading") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Loading row: shown when no rows exist yet (first fetch) -->
|
||||
<template #loading>
|
||||
<div
|
||||
data-test="services-catalog-loading"
|
||||
class="tw:flex tw:flex-nowrap tw:items-center tw:px-2 tw:min-w-max tw:min-h-[3.25rem] tw:bg-[var(--o2-card-bg)] tw:border-b tw:border-[var(--o2-border-2)]!"
|
||||
>
|
||||
<OSpinner size="xs" class="tw:mr-[0.25rem]" />
|
||||
<span
|
||||
class="tw:tracking-[0.03rem] tw:text-[0.85rem] tw:text-[var(--o2-text-1)] tw:font-bold"
|
||||
>
|
||||
{{ t("traces.servicesCatalog.loading") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Cell actions overlay -->
|
||||
<template #cell-actions="{ row, column, active }">
|
||||
<CellActions
|
||||
|
||||
@@ -131,12 +131,13 @@ vi.mock("@/components/TenstackTable.vue", () => ({
|
||||
setup() {
|
||||
return { cellActionsColumn: cellActionsColumnRef };
|
||||
},
|
||||
// Mirror the real slot behaviour used by TracesSearchResultList.
|
||||
// Mirror the real skeleton behaviour used by TenstackTable.
|
||||
// Also render cell slots for the first row so slot-split tests can assert on rendered output.
|
||||
template: `
|
||||
<div data-test="stub-traces-table">
|
||||
<slot v-if="loading && (!rows || rows.length === 0)" name="loading" />
|
||||
<slot v-else name="empty" />
|
||||
<div v-if="loading && (!$slots.loading)" data-test="tenstack-table-skeleton-body">Skeleton loading...</div>
|
||||
<slot v-if="loading && (!rows || rows.length === 0) && $slots.loading" name="loading" />
|
||||
<slot v-else-if="!loading" name="empty" />
|
||||
<template v-if="rows && rows.length">
|
||||
<div data-test="stub-cell-span_status"><slot name="cell-span_status" :item="rows[0]" /></div>
|
||||
<div data-test="stub-cell-status"><slot name="cell-status" :item="rows[0]" /></div>
|
||||
@@ -214,10 +215,10 @@ describe("TracesSearchResultList", () => {
|
||||
|
||||
// ─── Loading state ────────────────────────────────────────────────────────
|
||||
describe("loading state", () => {
|
||||
it("shows a spinner while loading", () => {
|
||||
it("shows skeleton while loading", () => {
|
||||
wrapper = mount_({ hits: [], loading: true });
|
||||
expect(
|
||||
wrapper.find('[data-test="traces-search-result-loading-indicator"]').exists(),
|
||||
wrapper.find('[data-test="tenstack-table-skeleton-body"]').exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -81,42 +81,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Loading banner: shown above rows while a new page is fetching -->
|
||||
<template #loading-banner>
|
||||
<div
|
||||
data-test="traces-table-loading-banner-row"
|
||||
class="tw:flex tw:flex-nowrap tw:items-center tw:px-2 tw:min-w-max tw:min-h-[3.25rem] tw:bg-[var(--o2-card-bg)] tw:border-b tw:border-[var(--o2-border-2)]!"
|
||||
>
|
||||
<OSpinner
|
||||
size="xs"
|
||||
class="tw:mx-[0.25rem]"
|
||||
data-test="traces-search-result-loading-indicator"
|
||||
/>
|
||||
<span
|
||||
class="tw:tracking-[0.03rem] tw:text-[0.85rem] tw:text-[var(--o2-text-1)] tw:font-bold"
|
||||
>{{ t("traces.fetchingTraces") }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Loading row: shown when no rows exist yet (first fetch) -->
|
||||
<template #loading>
|
||||
<div
|
||||
data-test="traces-table-loading-row"
|
||||
class="tw:flex tw:flex-nowrap tw:items-center tw:px-2 tw:min-w-max tw:min-h-[3.25rem] tw:bg-[var(--o2-card-bg)] tw:border-b tw:border-[var(--o2-border-2)]!"
|
||||
>
|
||||
<OSpinner
|
||||
size="xs"
|
||||
class="tw:mr-[0.25rem]"
|
||||
data-test="traces-search-result-loading-indicator"
|
||||
/>
|
||||
<span
|
||||
class="tw:tracking-[0.03rem] tw:text-[0.85rem] tw:text-[var(--o2-text-1)] tw:font-bold"
|
||||
>{{ t("traces.fetchingTraces") }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
#[`cell-${store.state.zoConfig.timestamp_column}`]="{ cell }"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user