mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
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:
co-authored by
Kirtan Thakkar
Bhargav
ktx-riya
parent
ff8a579bdc
commit
d79bbbe9e8
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("convertLogData.ts", () => {
|
||||
left: "20",
|
||||
right: "20",
|
||||
top: "5",
|
||||
bottom: "0",
|
||||
bottom: "5",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const convertLogData = (
|
||||
left: "20",
|
||||
right: "20",
|
||||
top: "5",
|
||||
bottom: "0",
|
||||
bottom: "5",
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
|
||||
Reference in New Issue
Block a user