From d79bbbe9e884aea8e4e51b9a30cd4d43c91bd648 Mon Sep 17 00:00:00 2001 From: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:59:57 +0530 Subject: [PATCH] fix: layout bugs 2 (#12594) Co-authored-by: Kirtan Thakkar Co-authored-by: Bhargav Co-authored-by: ktx-riya --- web/src/components/DateTime.vue | 2 +- .../dashboards/addPanel/ChartSelection.vue | 19 ++- web/src/components/iam/IngestionTokens.vue | 3 +- .../components/logs/useQueryPlaceholder.ts | 131 +++++++++--------- .../core/EmptyState/EmptyStateActionCard.vue | 8 +- .../lib/core/Table/OTable.flexwidth.spec.ts | 14 +- web/src/lib/core/Table/OTable.vue | 62 +-------- .../core/Table/composables/useTableCore.ts | 24 +++- web/src/plugins/logs/DetailTable.vue | 4 +- web/src/plugins/logs/SearchBar.vue | 13 +- web/src/plugins/logs/SearchJobInspector.vue | 15 +- .../components/TraceServiceCell.spec.ts | 21 ++- .../traces/components/TraceServiceCell.vue | 5 +- web/src/styles/logs/detail-table.scss | 5 +- web/src/utils/logs/convertLogData.spec.ts | 2 +- web/src/utils/logs/convertLogData.ts | 2 +- 16 files changed, 170 insertions(+), 160 deletions(-) diff --git a/web/src/components/DateTime.vue b/web/src/components/DateTime.vue index 73fb2972c1..159bb7209f 100644 --- a/web/src/components/DateTime.vue +++ b/web/src/components/DateTime.vue @@ -176,7 +176,7 @@ along with this program. If not, see . @update:end-date="selectedDate.to = $event" /> -
* You can choose multiple date
+
{{ t("common.datetimeMessage") }}
diff --git a/web/src/components/dashboards/addPanel/ChartSelection.vue b/web/src/components/dashboards/addPanel/ChartSelection.vue index 1738af998d..c29f7a3c17 100644 --- a/web/src/components/dashboards/addPanel/ChartSelection.vue +++ b/web/src/components/dashboards/addPanel/ChartSelection.vue @@ -23,11 +23,8 @@ along with this program. If not, see . :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); +} diff --git a/web/src/components/iam/IngestionTokens.vue b/web/src/components/iam/IngestionTokens.vue index c686e1abe1..7dd151550c 100644 --- a/web/src/components/iam/IngestionTokens.vue +++ b/web/src/components/iam/IngestionTokens.vue @@ -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", diff --git a/web/src/components/logs/useQueryPlaceholder.ts b/web/src/components/logs/useQueryPlaceholder.ts index 4132894ca9..82e49792b3 100644 --- a/web/src/components/logs/useQueryPlaceholder.ts +++ b/web/src/components/logs/useQueryPlaceholder.ts @@ -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 { diff --git a/web/src/lib/core/EmptyState/EmptyStateActionCard.vue b/web/src/lib/core/EmptyState/EmptyStateActionCard.vue index da1ee6dd2f..01f9b5e4d9 100644 --- a/web/src/lib/core/EmptyState/EmptyStateActionCard.vue +++ b/web/src/lib/core/EmptyState/EmptyStateActionCard.vue @@ -32,9 +32,11 @@ along with this program. If not, see . - {{ - label - }} + {{ label }} { 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); }); diff --git a/web/src/lib/core/Table/OTable.vue b/web/src/lib/core/Table/OTable.vue index 2517170dd6..96e44e588e 100644 --- a/web/src/lib/core/Table/OTable.vue +++ b/web/src/lib/core/Table/OTable.vue @@ -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>({}); -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 = {}; - for (const col of actionCols) { - const safeId = col.id.replace(/[^a-zA-Z0-9]/g, "-"); - const cells = root.querySelectorAll( - `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); diff --git a/web/src/lib/core/Table/composables/useTableCore.ts b/web/src/lib/core/Table/composables/useTableCore.ts index 46b2b392f6..5b6baeac1c 100644 --- a/web/src/lib/core/Table/composables/useTableCore.ts +++ b/web/src/lib/core/Table/composables/useTableCore.ts @@ -122,6 +122,19 @@ export function useTableCore( const isRigidColumn = (col: OTableColumnDef) => 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([]) as Ref; @@ -176,11 +189,16 @@ export function useTableCore( const tanstackColumns = computed[]>(() => { 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 = { id: col.id, header: col.header as any, diff --git a/web/src/plugins/logs/DetailTable.vue b/web/src/plugins/logs/DetailTable.vue index 7bfb2b56c4..56ce9138f3 100644 --- a/web/src/plugins/logs/DetailTable.vue +++ b/web/src/plugins/logs/DetailTable.vue @@ -232,10 +232,10 @@ along with this program. If not, see .
.
               :class="localSavedViews.length > 0 ? 'tw:border-r tw:border-[var(--o2-border-color)]' : ''"
               :style="localSavedViews.length > 0 ? 'width: 60%' : 'width: 100%'"
             >
-              
+
. class="tw:flex tw:flex-col tw:w-[40%] tw:ml-0 tw:pl-3" v-if="localSavedViews.length > 0" > -
+
. + +