fix: layout bugs 2 (#12594)

Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
Co-authored-by: Bhargav <BJP232004@GMAIL.COM>
Co-authored-by: ktx-riya <riya.kukadiya@kiara.tech>
This commit is contained in:
ktx-vaidehi
2026-06-11 12:59:57 +05:30
committed by GitHub
co-authored by Kirtan Thakkar Bhargav ktx-riya
parent ff8a579bdc
commit d79bbbe9e8
16 changed files with 170 additions and 160 deletions
+1 -1
View File
@@ -176,7 +176,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:end-date="selectedDate.to = $event"
/>
</div>
<div class="notePara">* You can choose multiple date</div>
<div class="notePara">{{ t("common.datetimeMessage") }}</div>
<OSeparator v-if="!disableRelative" class="tw:my-2" />
<table v-if="!hideRelativeTime" class="tw:px-3 startEndTime">
@@ -23,11 +23,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:key="index"
:class="[
'dashboard-chart-border',
selectedChartType === item.id
? store.state.theme === 'dark'
? 'tw:bg-gray-400'
: 'tw:bg-gray-200'
: '',
'chart-selection-item',
selectedChartType === item.id ? 'chart-item-selected' : '',
isChartDisabled(item)
? 'tw:opacity-50 tw:cursor-not-allowed'
: 'tw:cursor-pointer',
@@ -246,4 +243,16 @@ export default defineComponent({
padding: 0;
margin: 0;
}
.chart-selection-item {
transition: background-color 0.15s ease;
}
.chart-selection-item:hover {
background-color: var(--color-surface-subtle);
}
.chart-item-selected {
background-color: var(--color-border-subtle);
}
</style>
+2 -1
View File
@@ -247,7 +247,8 @@ export default defineComponent({
sortable: false,
resizable: true,
hideable: true,
size: COL.token,
// Wide enough for the truncated token (code max-w 280) + gap + copy btn.
size: 340,
},
{
id: "created_by",
+67 -64
View File
@@ -37,24 +37,6 @@ const SYSTEM_FIELDS = new Set([
const NUMERIC_TYPES = new Set(["Int64", "Float64", "UInt64", "Int32", "UInt32"]);
// Rotate through several example patterns per FTS field so the animation feels varied
function makeFtsExamples(val: string | undefined): string[] {
const term = val ?? "error";
return [
`match_all('${term}')`, // inverted index search
`match_all('${term}*')`, // prefix search
`match_all('*${term}')`, // postfix search
];
}
function makeColumnExamples(name: string, val: string): string[] {
return [
`str_match(${name}, '${val}')`, // column search
`str_match_ignore_case(${name}, '${val}')`, // case-insensitive column search
`fuzzy_match(${name}, '${val}', 1)`, // fuzzy match
];
}
/**
* Builds animated typewriter example queries from real stream field/value data.
* Priority: interesting (key) fields → FTS fields → other fields.
@@ -104,7 +86,6 @@ export function useQueryPlaceholder(
const fv = fieldValues.value ?? {};
const isSql = sqlMode.value;
// Exclude group header rows (label: true) — they are section dividers, not fields
const realFields = f.filter((x) => !x.label);
const interesting = realFields.filter(
@@ -114,11 +95,13 @@ export function useQueryPlaceholder(
(x) => x.ftsKey && !SYSTEM_FIELDS.has(x.name) && !x.isInterestingField,
);
const others = realFields
.filter(
(x) => !x.isInterestingField && !x.ftsKey && !SYSTEM_FIELDS.has(x.name),
)
.filter((x) => !x.isInterestingField && !x.ftsKey && !SYSTEM_FIELDS.has(x.name))
.slice(0, 3);
const strFields = realFields.filter(
(x) => !SYSTEM_FIELDS.has(x.name) && !NUMERIC_TYPES.has(x.dataType ?? ""),
).slice(0, 3);
const fieldExpr = (field: StreamField): string => {
const name = field.name;
const vals = fv[name]?.values ?? [];
@@ -137,57 +120,77 @@ export function useQueryPlaceholder(
return firstVal != null ? `${name} = '${firstVal}'` : `${name} = 'value'`;
};
const makeKeyExample = (field: StreamField, pairField?: StreamField): string | null => {
if (isSql) {
const base = `SELECT * FROM stream WHERE ${fieldExprSql(field)}`;
if (pairField) {
const op = Math.random() < 0.5 ? "AND" : "OR";
return `${base} ${op} ${fieldExprSql(pairField)}`;
}
return base;
}
if (pairField) {
const op = Math.random() < 0.5 ? " AND " : " OR ";
return `${fieldExpr(field)}${op}${fieldExpr(pairField)}`;
}
return fieldExpr(field);
};
const makeFtsExample = (field: StreamField, variant: number): string | null => {
if (isSql) return null;
const ftsVal = (field: StreamField): string => {
const vals = fv[field.name]?.values ?? [];
const firstVal = vals[0]?.key;
const examples = makeFtsExamples(firstVal);
return examples[variant % examples.length] ?? null;
return vals[0]?.key ?? "error";
};
const strVal = (field: StreamField): string => {
const vals = fv[field.name]?.values ?? [];
return vals[0]?.key ?? "error";
};
// Build alternating list: key field, fts variant, key field, fts variant, …
const keyFields = [...interesting, ...others.filter((x) => !x.ftsKey)].slice(0, 4);
const allFts = [...interesting.filter((x) => x.ftsKey), ...ftsOnly].slice(0, 4);
const kf0 = keyFields[0];
const kf1 = keyFields[1];
const ff0 = allFts[0];
const sf0 = strFields[0];
const result: string[] = [];
const len = Math.max(keyFields.length, allFts.length, 1);
for (let i = 0; i < len && result.length < 8; i++) {
const kf = keyFields[i % keyFields.length];
const ff = allFts[i % (allFts.length || 1)];
// Every other iteration pair with the next key field for AND/OR examples
const pairKf = keyFields.length > 1 && i % 2 === 1
? keyFields[(i + 1) % keyFields.length]
: undefined;
if (kf) {
const ex = makeKeyExample(kf, pairKf);
if (ex) result.push(ex);
}
if (ff && !isSql) {
const ex = makeFtsExample(ff, i);
if (ex) result.push(ex);
if (isSql) {
const result: string[] = [];
if (kf0) {
const base = `SELECT * FROM stream WHERE ${fieldExprSql(kf0)}`;
result.push(base);
if (kf1) result.push(`${base} AND ${fieldExprSql(kf1)}`);
if (kf1) result.push(`${base} OR ${fieldExprSql(kf1)}`);
if (ff0) result.push(`SELECT * FROM stream WHERE match_all('${ftsVal(ff0)}')`);
if (ff0 && kf1) result.push(`SELECT * FROM stream WHERE ${fieldExprSql(kf0)} AND match_all('${ftsVal(ff0)}')`);
}
return result;
}
return result;
// Filter mode — interleaved variations covering raw filters, match_all, and other functions
const result: string[] = [];
const term = ff0 ? ftsVal(ff0) : "error";
// 1. Simple raw filter
if (kf0) result.push(fieldExpr(kf0));
// 2. match_all exact
result.push(`match_all('${term}')`);
// 3. Raw filter AND match_all
if (kf0) result.push(`${fieldExpr(kf0)} AND match_all('${term}')`);
// 4. match_all prefix variant
result.push(`match_all('${term}*')`);
// 5. Two raw filters AND match_all (covers AND + OR operators together)
if (kf0 && kf1) result.push(`${fieldExpr(kf0)} AND ${fieldExpr(kf1)} AND match_all('${term}')`);
else if (kf0) result.push(`${fieldExpr(kf0)} OR match_all('${term}')`);
// 6. match_all postfix variant
result.push(`match_all('*${term}')`);
// 7. str_match combined with match_all prefix
if (sf0) {
result.push(`str_match(${sf0.name}, '${strVal(sf0)}') AND match_all('${term}*')`);
} else if (kf0) {
result.push(`${fieldExpr(kf0)} AND match_all('${term}*')`);
}
// 8. fuzzy_match combined with raw filter
if (sf0 && kf0 && sf0.name !== kf0.name) {
result.push(`${fieldExpr(kf0)} AND fuzzy_match(${sf0.name}, '${strVal(sf0)}', 1)`);
} else if (sf0) {
result.push(`fuzzy_match(${sf0.name}, '${strVal(sf0)}', 1)`);
}
// 9. Raw filter OR match_all postfix
if (kf0) result.push(`${fieldExpr(kf0)} OR match_all('*${term}')`);
return result.slice(0, 9);
});
async function loop(): Promise<void> {
@@ -32,9 +32,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</span>
<span class="tw:relative tw:flex-1 tw:min-w-0">
<span class="tw:block tw:text-sm tw:font-semibold tw:truncate tw:text-text-primary">{{
label
}}</span>
<span
:title="label"
class="tw:block tw:text-sm tw:font-semibold tw:text-text-primary tw:wrap-break-word"
>{{ label }}</span
>
<span
v-if="sublabel"
class="tw:block tw:text-xs tw:text-text-secondary tw:leading-snug"
@@ -69,26 +69,26 @@ describe("OTable flex column width", () => {
expect(tableWidth(wrapper)).toBe(-1);
// Resize freezes the flex column at its current fill width. With no layout,
// the arithmetic fallback = container − nonFlex (1000 − 268) = 732 is pinned
// the arithmetic fallback = container − nonFlex (1000 − 272) = 728 is pinned
// into columnSizing. Then widen Type to 400 (TanStack preserves name).
st.freezeFlexColumns();
st.columnSizing = { ...st.columnSizing, type: 400 };
await flushPromises();
// Now explicit width: Name HOLDS at 732, table grows to 1300 (scroll),
// Now explicit width: Name HOLDS at 728, table grows to 1300 (scroll),
// # stays 44, and w-full is dropped.
expect(isWFull(wrapper)).toBe(false);
expect(colVar(wrapper, "name")).toBe(732);
expect(colVar(wrapper, "name")).toBe(728);
expect(tableWidth(wrapper)).toBe(1300);
expect(colVar(wrapper, "#")).toBe(44);
// DECREASE Type to 50 (below its original 100). Name must NOT grow to
// absorb the freed space — it stays frozen at 732. Real columns sum to 950
// absorb the freed space — it stays frozen at 728. Real columns sum to 950
// (< container) so the invisible spacer absorbs the 50px (its var) and the
// table stays at the container width (1000) → actions flush-right.
st.columnSizing = { ...st.columnSizing, type: 50 };
await flushPromises();
expect(colVar(wrapper, "name")).toBe(732);
expect(colVar(wrapper, "name")).toBe(728);
expect(colVar(wrapper, "#")).toBe(44);
expect(colVar(wrapper, "__spacer__")).toBe(50);
expect(tableWidth(wrapper)).toBe(1000);
@@ -115,9 +115,9 @@ describe("OTable flex column width", () => {
(wrapper.vm.$ as any).setupState.containerWidth = 1000;
await flushPromises();
// The flex (name) column fills explicitly (container 1000 − nonFlex 268).
// The flex (name) column fills explicitly (container 1000 − nonFlex 272).
expect(thHasExplicitWidth(wrapper, "name")).toBe(true);
expect(headerVar(wrapper, "name")).toBe(732);
expect(headerVar(wrapper, "name")).toBe(728);
// The invisible spacer must be 0 in the fill state — no trailing gap.
expect(headerVar(wrapper, "--spacer--")).toBe(0);
});
+7 -55
View File
@@ -372,55 +372,12 @@ const isVirtual = computed(() => props.virtualScroll && displayRows.value.length
// slot), so after each render we measure the rendered action cells and pin the
// column's CSS size var to the widest content. These vars override the nominal
// sizes computed in useTableCore.
// Per-column CSS size-var overrides. The actions column is NO LONGER measured
// from the DOM at runtime (it's sized deterministically from its icon count in
// useTableCore, so the skeleton and loaded table match) — this stays empty for
// now but is kept as the override channel the table style/sums read from.
const measuredColumnSizeVars = ref<Record<string, string>>({});
function measureActionColumns() {
const root = scrollContainerRef.value;
if (!root) return;
const actionCols = effectiveColumns.value.filter(
(c) => c.isAction === true || c.id === "actions",
);
if (!actionCols.length) {
if (Object.keys(measuredColumnSizeVars.value).length) {
measuredColumnSizeVars.value = {};
}
return;
}
const next: Record<string, string> = {};
for (const col of actionCols) {
const safeId = col.id.replace(/[^a-zA-Z0-9]/g, "-");
const cells = root.querySelectorAll<HTMLElement>(
`td[data-test="o2-table-cell-${col.id}"]`,
);
if (!cells.length) continue;
let contentMax = 0;
let padX = 16; // fallback: tw:px-2 (8px each side)
cells.forEach((cell, i) => {
if (i === 0) {
const cs = getComputedStyle(cell);
padX = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
}
// The slot wrapper is inline-flex (natural content width) even when the
// td is momentarily narrower, so its rect is the true content width.
const wrapper = cell.firstElementChild as HTMLElement | null;
const w = wrapper
? wrapper.getBoundingClientRect().width
: cell.getBoundingClientRect().width;
if (w > contentMax) contentMax = w;
});
if (contentMax > 0) {
const total = `${Math.ceil(contentMax + padX) + 1}px`;
next[`--header-${safeId}-size`] = total;
next[`--col-${col.id}-size`] = total;
}
}
const cur = measuredColumnSizeVars.value;
const changed =
Object.keys(next).length !== Object.keys(cur).length ||
Object.keys(next).some((k) => next[k] !== cur[k]);
if (changed) measuredColumnSizeVars.value = next;
}
// ── Flex-fill measurement ───────────────────────────────────────
// While filling (not frozen), the flex column's width = the container's
// clientWidth minus the ACTUAL rendered widths of every other header cell.
@@ -462,14 +419,9 @@ function measureFlexFill() {
}
function scheduleMeasureActions() {
nextTick(() =>
requestAnimationFrame(() => {
measureActionColumns();
// Re-measure flex on the next frame so the actions column's new width is
// already painted before we subtract it.
requestAnimationFrame(measureFlexFill);
}),
);
// The actions column is sized deterministically (no DOM measurement), so only
// the flex fill needs measuring. Do it after the DOM settles.
nextTick(() => requestAnimationFrame(measureFlexFill));
}
onMounted(scheduleMeasureActions);
@@ -122,6 +122,19 @@ export function useTableCore<TData>(
const isRigidColumn = (col: OTableColumnDef<TData>) =>
col.isAction === true || col.id === "actions" || col.id === "#";
// Deterministic actions-column width from its icon count: N icon buttons
// (icon-sm = 32px) + gaps + cell padding, never narrower than the "Actions"
// header. Computed (not DOM-measured) so the skeleton and loaded table match.
const ACTION_ICON_BTN = 32; // OButton size="icon-sm" → w-8
const ACTION_BTN_GAP = 4; // gap-1 between buttons
const ACTION_CELL_PAD = 16; // td px-2
const ACTIONS_HEADER_MIN = 80; // enough to show the "Actions" header in full
const actionColumnWidth = (actionCount?: number): number => {
const n = Math.max(1, Number(actionCount) || 2);
const content = n * ACTION_ICON_BTN + (n - 1) * ACTION_BTN_GAP + ACTION_CELL_PAD;
return Math.max(content, ACTIONS_HEADER_MIN);
};
// Track column order for drag-reorder
const columnOrder = ref<string[]>([]) as Ref<string[]>;
@@ -176,11 +189,16 @@ export function useTableCore<TData>(
const tanstackColumns = computed<ColumnDef<TData>[]>(() => {
return effectiveColumns.value.map((col) => {
const rigid = isRigidColumn(col);
const isActionCol = col.isAction === true || col.id === "actions";
// Rigid columns (selection index, actions) hold a width that does not
// depend on the other columns: min === size === max so the table layout
// can neither grow nor shrink them. The actions column's `size` is later
// overridden at runtime by a content measurement in OTable.vue.
const size = col.size ?? 150;
// can neither grow nor shrink them. The actions column's width is computed
// DETERMINISTICALLY from its icon count (not DOM-measured at runtime) so
// the loading skeleton and the loaded table render it at the exact same
// width — no flash when data arrives.
const size = isActionCol
? Math.max(col.size ?? 0, actionColumnWidth((col.meta as any)?.actionCount))
: (col.size ?? 150);
const columnDef: ColumnDef<TData> = {
id: col.id,
header: col.header as any,
+2 -2
View File
@@ -232,10 +232,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</ODropdown>
<pre
:data-test="`log-detail-${row.field}-value`"
class="table-pre tw:flex-1"
class="table-pre tw:flex-1 tw:min-w-0"
:class="
!shouldWrapValues
? 'tw:whitespace-nowrap'
? 'tw:whitespace-nowrap tw:overflow-hidden tw:text-ellipsis'
: 'tw:whitespace-pre-wrap'
"
><ChunkedContent
+9 -4
View File
@@ -1432,7 +1432,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:class="localSavedViews.length > 0 ? 'tw:border-r tw:border-[var(--o2-border-color)]' : ''"
:style="localSavedViews.length > 0 ? 'width: 60%' : 'width: 100%'"
>
<div style="max-height: 486px; min-height: 280px; overflow: hidden; display: flex; flex-direction: column;">
<div style="max-height: 486px; min-height: 280px; display: flex; flex-direction: column;">
<OTable
data-test="log-search-saved-view-list-fields-table"
:data="searchObj.data.savedViews"
@@ -1541,7 +1541,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="tw:flex tw:flex-col tw:w-[40%] tw:ml-0 tw:pl-3"
v-if="localSavedViews.length > 0"
>
<div style="max-height: 480px; min-height: 280px; overflow: hidden; display: flex; flex-direction: column;">
<div style="max-height: 480px; min-height: 280px; display: flex; flex-direction: column;">
<OTable
data-test="log-search-saved-view-favorite-list-fields-table"
:data="localSavedViews"
@@ -4163,8 +4163,8 @@ export default defineComponent({
return;
}
localSavedView[row.view_id] = JSON.parse(JSON.stringify(row));
favoriteViews.value.push(row.view_id);
localSavedViews.value.push(row);
favoriteViews.value = [...favoriteViews.value, row.view_id];
localSavedViews.value = [...localSavedViews.value, row];
// moveItemsToTop(localSavedView, favoriteViews.value);
@@ -5580,6 +5580,11 @@ html.dark .file-type label,
border-top: none;
}
// Hide the redundant total-count chip on the left — "of N" on the right already shows it
.saved-view-table :deep([data-test="o2-table-pagination-bottom"] .o2-table-footer-title) {
display: none;
}
// VRL disabled warning background — theme-aware via CSS cascade
.vrl-disabled-warning {
background-color: rgba(0, 0, 0, 0.1);
+11 -4
View File
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="tw:rounded-md search-job-inspector tw:p-0">
<div class="tw:w-full tw:flex tw:flex-col tw:h-[calc(100vh_-_var(--navbar-height))] tw:overflow-hidden">
<div class="tw:w-full tw:flex tw:flex-col tw:h-full tw:overflow-hidden">
<!-- Header Card -->
<div class="card-container tw:mb-[0.625rem] tw:mt-[0.325rem] tw:mx-2.5 tw:shrink-0">
<div class="tw:flex tw:justify-between tw:w-full tw:py-3 tw:px-4 tw:items-center">
@@ -277,6 +277,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
tree
tree-column-id="index"
:default-columns="false"
:enable-column-resize="true"
:persist-columns="true"
table-id="logs-search-job-inspector"
style="width: 100%;"
class="o2-quasar-table o2-row-md o2-quasar-table-header-sticky"
data-test="inspector-events-table"
@@ -298,8 +301,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</template>
<template #cell-component="{ row }">
<span :title="row.component">{{ row.component }}</span>
</template>
<template #cell-desc="{ row }">
<div class="tw:text-xs">{{ row.desc || '-' }}</div>
<span class="tw:text-xs" :title="row.desc || '-'">{{ row.desc || '-' }}</span>
</template>
<template #empty>
@@ -455,7 +462,7 @@ export default defineComponent({
header: "Node Name",
accessorKey: "node_name",
meta: { align: "left" },
size: 200,
size: 280,
},
{
id: "search_role",
@@ -469,7 +476,7 @@ export default defineComponent({
header: "Operation",
accessorKey: "component",
meta: { align: "left" },
size: 250,
size: 340,
},
{
id: "desc",
@@ -15,6 +15,7 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { mount, VueWrapper } from "@vue/test-utils";
import { createStore } from "vuex";
const mockServiceColors: Record<string, string> = {
frontend: "#4caf50",
@@ -48,6 +49,7 @@ vi.mock("@/utils/traces/convertTraceData", () => ({
import TraceServiceCell from "./TraceServiceCell.vue";
import { getServiceIconDataUrl } from "@/utils/traces/convertTraceData";
const store = createStore({ state: { theme: "light" } });
const makeItem = (overrides: Record<string, any> = {}) => ({
service_name: "frontend",
@@ -66,19 +68,19 @@ describe("TraceServiceCell", () => {
describe("rendering", () => {
it("mounts without errors", () => {
wrapper = mount(TraceServiceCell, { props: { item: makeItem() } });
wrapper = mount(TraceServiceCell, { props: { item: makeItem() }, global: { plugins: [store] } });
expect(wrapper.exists()).toBe(true);
});
it("renders the service row container", () => {
wrapper = mount(TraceServiceCell, { props: { item: makeItem() } });
wrapper = mount(TraceServiceCell, { props: { item: makeItem() }, global: { plugins: [store] } });
expect(wrapper.find('[data-test="trace-row-service"]').exists()).toBe(
true,
);
});
it("renders service name", () => {
wrapper = mount(TraceServiceCell, { props: { item: makeItem() } });
wrapper = mount(TraceServiceCell, { props: { item: makeItem() }, global: { plugins: [store] } });
const name = wrapper.find('[data-test="trace-row-service-name"]');
expect(name.exists()).toBe(true);
expect(name.text()).toBe("frontend");
@@ -87,7 +89,7 @@ describe("TraceServiceCell", () => {
// TraceServiceCell only renders service name and icon.
// Operation name is rendered by a separate column cell — not this component.
it.skip("renders operation name", () => {
wrapper = mount(TraceServiceCell, { props: { item: makeItem() } });
wrapper = mount(TraceServiceCell, { props: { item: makeItem() }, global: { plugins: [store] } });
const op = wrapper.find('[data-test="trace-row-operation-name"]');
expect(op.exists()).toBe(true);
expect(op.text()).toBe("GET /api/v1/users");
@@ -96,20 +98,20 @@ describe("TraceServiceCell", () => {
describe("service icon", () => {
it("should render the service icon img", () => {
wrapper = mount(TraceServiceCell, { props: { item: makeItem() } });
wrapper = mount(TraceServiceCell, { props: { item: makeItem() }, global: { plugins: [store] } });
expect(
wrapper.find('[data-test="trace-row-service-icon"]').exists(),
).toBe(true);
});
it("should set the icon src from getServiceIconDataUrl", () => {
wrapper = mount(TraceServiceCell, { props: { item: makeItem() } });
wrapper = mount(TraceServiceCell, { props: { item: makeItem() }, global: { plugins: [store] } });
const img = wrapper.find('[data-test="trace-row-service-icon"]');
expect(img.attributes("src")).toBe("data:image/svg+xml;base64,TEST");
});
it("should call getServiceIconDataUrl with service name and color", () => {
wrapper = mount(TraceServiceCell, { props: { item: makeItem() } });
wrapper = mount(TraceServiceCell, { props: { item: makeItem() }, global: { plugins: [store] } });
expect(vi.mocked(getServiceIconDataUrl)).toHaveBeenCalledWith(
"frontend",
expect.any(Boolean),
@@ -120,6 +122,7 @@ describe("TraceServiceCell", () => {
it("should use fallback color #9e9e9e for unknown service", () => {
wrapper = mount(TraceServiceCell, {
props: { item: makeItem({ service_name: "unknown-svc" }) },
global: { plugins: [store] },
});
expect(vi.mocked(getServiceIconDataUrl)).toHaveBeenCalledWith(
"unknown-svc",
@@ -135,6 +138,7 @@ describe("TraceServiceCell", () => {
props: {
item: makeItem({ services: { frontend: { duration: 100 } } }),
},
global: { plugins: [store] },
});
expect(
wrapper.find('[data-test="trace-row-extra-services"]').exists(),
@@ -149,6 +153,7 @@ describe("TraceServiceCell", () => {
services: { frontend: { duration: 100 } },
}),
},
global: { plugins: [store] },
});
expect(
wrapper.find('[data-test="trace-row-extra-services"]').exists(),
@@ -160,6 +165,7 @@ describe("TraceServiceCell", () => {
it("handles undefined services gracefully", () => {
wrapper = mount(TraceServiceCell, {
props: { item: { service_name: "frontend", operation_name: "op" } },
global: { plugins: [store] },
});
expect(wrapper.find('[data-test="trace-row-service-name"]').text()).toBe(
"frontend",
@@ -172,6 +178,7 @@ describe("TraceServiceCell", () => {
it("handles empty services object", () => {
wrapper = mount(TraceServiceCell, {
props: { item: makeItem({ services: {} }) },
global: { plugins: [store] },
});
expect(
wrapper.find('[data-test="trace-row-extra-services"]').exists(),
@@ -42,6 +42,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { computed } from "vue";
import { useStore } from "vuex";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import useTraces from "@/composables/useTraces";
import { getServiceIconDataUrl } from "@/utils/traces/convertTraceData";
@@ -51,6 +52,8 @@ const props = defineProps<{
dataTest?: string;
}>();
const store = useStore();
const { getOrSetServiceColor } = useTraces();
const rootColor = computed(
@@ -60,7 +63,7 @@ const rootColor = computed(
const serviceIconUrl = computed(() =>
getServiceIconDataUrl(
props.item.service_name,
document.documentElement.classList.contains("dark"),
store.state.theme === "dark",
rootColor.value,
),
);
+4 -1
View File
@@ -11,7 +11,10 @@
}
.table-pre {
white-space: pre-wrap;
// `white-space` is intentionally not set here: the Wrap toggle drives it via
// the conditional `tw:whitespace-pre-wrap` / `tw:whitespace-nowrap` classes on
// the element. Hardcoding it here would override those (equal specificity, SCSS
// wins by source order) and make the toggle a no-op.
word-wrap: break-word;
display: inline;
font-weight: normal;
+1 -1
View File
@@ -86,7 +86,7 @@ describe("convertLogData.ts", () => {
left: "20",
right: "20",
top: "5",
bottom: "0",
bottom: "5",
});
});
+1 -1
View File
@@ -32,7 +32,7 @@ export const convertLogData = (
left: "20",
right: "20",
top: "5",
bottom: "0",
bottom: "5",
},
tooltip: {
show: true,