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:
Omkar Kesarkhane
2026-06-12 07:45:30 +00:00
committed by GitHub
parent 4ecc066ac1
commit 76bd5dd822
6 changed files with 273 additions and 211 deletions
@@ -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 }"
>