diff --git a/web/src/composables/useToolbarPins.ts b/web/src/composables/useToolbarPins.ts index ca96549e7f..a4ea586028 100644 --- a/web/src/composables/useToolbarPins.ts +++ b/web/src/composables/useToolbarPins.ts @@ -16,6 +16,7 @@ import { computed, ref } from "vue"; // Canonical keys for every pinnable item in the logs More menu. export type ToolbarPinKey = + | "histogram" | "sqlMode" | "quickMode" | "functionEditor" @@ -25,6 +26,7 @@ export type ToolbarPinKey = // Fixed left-to-right render order for pinned controls. Pinning never changes an // item's position — it only toggles whether the item is shown outside the menu. export const TOOLBAR_PIN_ORDER: ToolbarPinKey[] = [ + "histogram", "sqlMode", "quickMode", "functionEditor", @@ -34,18 +36,32 @@ export const TOOLBAR_PIN_ORDER: ToolbarPinKey[] = [ const STORAGE_KEY = "logs_toolbar_pinned_items"; +// Histogram was a fixed toolbar control before it became pinnable, so it is +// pinned by default. Its absence from STORAGE_KEY (including arrays persisted +// before histogram existed as a pin key) means "never decided", not "unpinned" — +// this flag records the user's first explicit pin/unpin of histogram, after +// which STORAGE_KEY alone is authoritative. +const HISTOGRAM_PIN_DECIDED_KEY = "logs_toolbar_histogram_pin_decided"; + const isValidKey = (key: string): key is ToolbarPinKey => (TOOLBAR_PIN_ORDER as string[]).includes(key); const readInitial = (): ToolbarPinKey[] => { try { const raw = window.localStorage.getItem(STORAGE_KEY); - if (!raw) return []; - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) return []; - return parsed.filter((k): k is ToolbarPinKey => typeof k === "string" && isValidKey(k)); + const parsed = raw ? JSON.parse(raw) : []; + const pins: ToolbarPinKey[] = Array.isArray(parsed) + ? parsed.filter( + (k): k is ToolbarPinKey => typeof k === "string" && isValidKey(k), + ) + : []; + const histogramDecided = + window.localStorage.getItem(HISTOGRAM_PIN_DECIDED_KEY) === "true"; + if (!histogramDecided && !pins.includes("histogram")) + pins.push("histogram"); + return pins; } catch { - return []; + return ["histogram"]; } }; @@ -72,6 +88,13 @@ export function useToolbarPins() { if (next.has(key)) next.delete(key); else next.add(key); pinnedSet.value = next; + if (key === "histogram") { + try { + window.localStorage.setItem(HISTOGRAM_PIN_DECIDED_KEY, "true"); + } catch { + // localStorage may be unavailable — the default-pinned fallback reapplies next session. + } + } persist(); }; diff --git a/web/src/plugins/logs/SearchBar.vue b/web/src/plugins/logs/SearchBar.vue index 69fdd6501b..441b5bd5fc 100644 --- a/web/src/plugins/logs/SearchBar.vue +++ b/web/src/plugins/logs/SearchBar.vue @@ -126,9 +126,14 @@ along with this program. If not, see . - + + + . - - . {{ t("search.resetFilters") }} - + @@ -304,13 +303,23 @@ along with this program. If not, see . {{ t("search.showHistogramLabel") }} @@ -2404,35 +2413,33 @@ export default defineComponent({ // Each threshold has a small buffer (+16px) so collapse fires before clipping. const shouldHideToolbarButtonText = computed(() => availableLeftWidth.value < 720); const toolbarToggleIconOnly = computed(() => availableLeftWidth.value < 568); - const shouldMoveButtonsToMenu = computed(() => availableLeftWidth.value < 328); const toolbarMoveResetToMenu = computed(() => availableLeftWidth.value < 248); const toolbarToggleAsDropdown = computed(() => availableLeftWidth.value < 176); // ── Pinned toolbar items ────────────────────────────────────────────── // Items pinned out of the "More" menu render as fixed-position toolbar - // controls. They share the left section with the toggle group / reset / - // histogram, so we allocate the leftover width to pinned items with a running - // budget: the More button is always reserved, then pinned items are kept in - // priority order (sql mode kept longest, syntax guide dropped first). + // controls. They share the left section with the toggle group / reset, so we + // allocate the leftover width to pinned items with a running budget: the More + // button is always reserved, then pinned items are kept in priority order + // (histogram kept longest, syntax guide dropped first). const { isPinned, togglePin } = useToolbarPins(); // Approximate rendered widths (px) of each pinned control and of the fixed // left-section content, used only to decide how many pinned items fit before // they would clip. Hidden pinned items stay reachable inside the More menu. - const PIN_ITEM_WIDTH = { sqlMode: 46, quickMode: 46, savedViews: 62 }; + const PIN_ITEM_WIDTH = { histogram: 46, sqlMode: 46, quickMode: 46, savedViews: 62 }; const SYNTAX_GUIDE_LABEL_WIDTH = 108; const SYNTAX_GUIDE_ICON_WIDTH = 40; const PIN_ITEM_GAP = 4; // Width consumed by the always-present left content (toggle group in its - // current collapse state, reset, histogram) plus the reserved More button. + // current collapse state, reset) plus the reserved More button. const baseReservedWidth = computed(() => { let w = 0; if (toolbarToggleAsDropdown.value) w += 120; else if (toolbarToggleIconOnly.value) w += 190; else w += 350; if (!toolbarMoveResetToMenu.value) w += shouldHideToolbarButtonText.value ? 40 : 88; - if (!shouldMoveButtonsToMenu.value) w += 64; // histogram button w += 92; // More button (always visible) w += 24; // inter-item gaps / padding buffer return w; @@ -2448,6 +2455,7 @@ export default defineComponent({ const budget = pinBudget.value; let used = 0; const res = { + histogram: false, sqlMode: false, quickMode: false, savedViews: false, @@ -2462,6 +2470,7 @@ export default defineComponent({ } return false; }; + if (isPinned("histogram")) res.histogram = tryFit(PIN_ITEM_WIDTH.histogram); if (isPinned("sqlMode")) res.sqlMode = tryFit(PIN_ITEM_WIDTH.sqlMode); if (isPinned("quickMode")) res.quickMode = tryFit(PIN_ITEM_WIDTH.quickMode); if (isPinned("savedViews")) res.savedViews = tryFit(PIN_ITEM_WIDTH.savedViews); @@ -2478,6 +2487,7 @@ export default defineComponent({ // Function editor lives on the right toolbar (next to the date picker), so it // is not part of the left-section budget. + const showPinnedHistogram = computed(() => pinnedVisibility.value.histogram); const showPinnedSqlMode = computed(() => pinnedVisibility.value.sqlMode); const showPinnedQuickMode = computed(() => pinnedVisibility.value.quickMode); const showPinnedFunctionEditor = computed(() => isPinned("functionEditor")); @@ -5217,12 +5227,12 @@ export default defineComponent({ toolbarRightRef, shouldHideToolbarButtonText, toolbarToggleIconOnly, - shouldMoveButtonsToMenu, toolbarMoveResetToMenu, toolbarToggleAsDropdown, isPinned, togglePin, pinSyntaxGuideIconOnly, + showPinnedHistogram, showPinnedSqlMode, showPinnedQuickMode, showPinnedFunctionEditor, diff --git a/web/src/plugins/logs/SearchResult.vue b/web/src/plugins/logs/SearchResult.vue index 87f24efd94..26d0ee99fb 100644 --- a/web/src/plugins/logs/SearchResult.vue +++ b/web/src/plugins/logs/SearchResult.vue @@ -349,22 +349,21 @@ along with this program. If not, see . -
+ -
- - No - data found for histogram. -
-
+ size="inline" + icon="bar-chart" + title="No Data" + :backdrop="false" + data-test="logs-search-no-data-histogram" + class="histogram-empty tw:!min-h-0 tw:!p-2" + />
import("./DetailTable.vue")), ChartRenderer: defineAsyncComponent(