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(