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:
@@ -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