feat(ui): responsive mobile/tablet support across the app (#13763)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
ktx-vaidehi
2026-09-11 14:00:35 +00:00
committed by GitHub
co-authored by github-actions[bot]
parent 82d76c8872
commit be23422e5b
248 changed files with 5621 additions and 860 deletions
+56 -9
View File
@@ -3,7 +3,7 @@ name: ui-architect
description: >-
Authoring guardrails for building ANY new frontend UI in the OpenObserve web
app (web/) — new views, pages, panels, dialogs, feature components, or edits
to existing ones. Enforce six house rules the moment you write Vue/template
to existing ones. Enforce seven house rules the moment you write Vue/template
markup: (1) use OPageHeader for every page/module header, (2) build UI from
O2 library components in web/src/lib — never bare HTML controls when an O2
equivalent exists, (3) NEVER write px — always rem, including inside Tailwind class
@@ -29,7 +29,12 @@ description: >-
is banned) with keys added to web/src/locales/languages/en-US.json; text-carrying
props/fields are typed I18nText and i18n keys stored as data are typed I18nKey,
and the ONLY opt-out for a genuinely non-translatable string is raw() — never an
eslint-disable. It also settles the recurring
eslint-disable, (7) every page is responsive — phones and tablets adapt through
max-md:/max-lg: variants and useBreakpoint() while the laptop (≥lg) view stays
identical; headers, toolbars and stat strips keep one row, filter toggles collapse
to dropdowns (mobile-dropdown), secondary header actions go to #actions-overflow,
side panels become drawers that open from their own row, row actions fold into a
kebab, and popups fit the viewport. It also settles the recurring
structural decisions: use OTable for any tabular data, follow the
view → service → Vuex/local-ref layering for fetching list data, choose the
right form container (ConfirmDialog vs ODialog vs ODrawer vs a full in-page
@@ -38,7 +43,8 @@ description: >-
v-model/ref mirrors, automatic submit/loading, correct field-array keys).
Trigger this whenever the user asks to create, add, build, scaffold, lay out,
validate, or restyle any screen, component, header, table, list, dialog,
drawer, form, field, or panel in the web frontend, or asks where a
drawer, form, field, or panel in the web frontend, asks to make any of them
work on mobile / phone / tablet / small screens (responsive), or asks where a
form/table/fetch should live, how to validate a form, how to add a keyboard
shortcut, how to build a new reusable/common O2 component when nothing existing
fits (create one in web/src/lib instead of assembling divs and classes),
@@ -61,7 +67,7 @@ token system, one spacing scale.
This skill governs **feature/app UI** — views under `web/src/views` and components
under `web/src/components` — built from the shared **O2 component library** in
`web/src/lib`. This page is the **contract + map**: the six laws and the
`web/src/lib`. This page is the **contract + map**: the seven laws and the
recurring structural decisions, each in a line or two, each pointing to the
reference that carries the full rationale, examples, and per-component detail.
Open the linked reference before you implement that specific thing — don't guess a
@@ -69,10 +75,11 @@ prop, a class string, or a path.
---
## The six house rules
## The seven house rules
The always-true laws. Each is stated here in brief; the full **what / why / how +
code** for all six is in [references/house-rules.md](references/house-rules.md) —
code** for rules 1–6 is in [references/house-rules.md](references/house-rules.md), and
rule 7 has its own [references/responsive.md](references/responsive.md) —
read it once, it is the backbone of everything below.
1. **Every page/module header is `OPageHeader`** — never a hand-rolled
@@ -513,6 +520,33 @@ read it once, it is the backbone of everything below.
> Toast/notification copy added by this convention lives under `toastMessages.*`,
> grouped by module.
7. **Every page is responsive — and the laptop view does not move.** New pages
are built for 375 px phones and 768 px tablets from the first commit, using the
method in [references/responsive.md](references/responsive.md):
- **Desktop is frozen.** Responsive rules are additive below a breakpoint —
`max-md:` (phone), `max-lg:` / `md:max-lg:` (tablet) — or a JS branch on
`useBreakpoint()`'s `isMobile` / `!lgUp`. An unprefixed class change or a bare
`md:`/`lg:` class that alters ≥1024 px is a blocker. JS is only for structure
(rail → drawer, toggle strip → dropdown, splitter locked shut).
- **One row of chrome.** Header: primaries in `#actions`, secondaries in
`#actions-overflow` (inline on desktop, one ⋮ below md; `overflow-first` when
they precede the CTA). Toolbar: wrapper `max-md:contents`, filter
`OToggleGroup mobile-dropdown`, search `min-w-0 flex-1 max-md:min-w-40`.
Stat tiles: `OStatStrip` / `KpiCard` (compact to icon + value below lg).
- **Side panels open from their own row.** `OPageLayout #sidebar` and
`FolderList` already become drawers; any other rail renders in an
`ODrawer` with `anchor` set to its trigger's row. Only the main nav opens from
the top.
- **Tables keep every column** and scroll within the frame (OTable does it);
inline row actions get `max-md:hidden` plus one `md:hidden` kebab mirroring them
with `<data-test>-menu` items; the footer count is `max-md:hidden`.
- **Nothing clipped, nothing hover-only.** Popups use library components and
`min(<w>, calc(100vw - 1.5rem))` widths; an `h-full` pane beside a stacked
sibling gets `max-md:h-auto max-md:min-h-0`; hover-revealed controls get
`max-md:opacity-100`.
- **Verify** at 375 / 360 / 768 / 1280 in the in-app browser, and at 1280 compare
against main — identical is the bar.
## Structural decisions
*What* to reach for and *where the code lives* — the recurring calls that
@@ -535,6 +569,7 @@ and each domain has its own reference below.
| **New page in nav** | a route **+ exactly one** surface (rail item / flyout child / Settings / IAM sub-page) **+** an env/role gate — the route condition, the nav-entry gate, and the SectionRail `visible` all express the same rule | [navigation-menus](references/navigation-menus.md) |
| **Keyboard shortcuts** | registry-driven — declare in `shortcutRegistry.ts`, bind with `useShortcuts([{ id, handler }])`; never an ad-hoc `keydown` listener or a hardcoded `⌘N` in a template | [keyboard-shortcuts](references/keyboard-shortcuts.md) |
| **Cancel / Save row** | cancel = `variant="outline"`, save = `variant="primary"`, both `size="sm-action"`, spaced with `gap-2` on the parent | [conventions](references/conventions.md) |
| **Responsive** | `max-md:`/`max-lg:` variants + `useBreakpoint()` for structure; desktop unchanged; one row of header/toolbar/stat chrome; rails → anchored drawers; row actions → kebab; popups ≤ viewport | [responsive](references/responsive.md) |
| **Nothing fits** | build a reusable component — generic primitive → a new `O*` in `web/src/lib`; app-specific composition → a named component in `web/src/components`. Never hand-assemble `<div>` + utility classes to fake a component | [creating-components](references/creating-components.md) |
**Dark mode is automatic** — every O2 component and token resolves correctly in
@@ -709,9 +744,21 @@ considering the UI done:
- [ ] `data-test` on every interactive and key output element, pattern
`<module>-<filename>-<descriptor>` (see the project FE rules).
- [ ] New component uses `<script setup lang="ts">`, no `// @ts-nocheck`.
- [ ] **Comments are one or two lines** — the *why* of a non-obvious choice, not
a re-telling of the code or the history of the PR that added it (no ticket
ids, "review finding", "as discussed"). Same in specs. See
- [ ] **Responsive, desktop untouched** — every new/changed class that affects
layout is `max-md:` / `max-lg:` gated (run the grep in
[responsive § The contract](references/responsive.md#the-contract)); the page
was checked at 375, 768 and 1280 and 1280 matches main.
- [ ] **Phone chrome is one row** — secondary header actions in
`#actions-overflow`, toolbar filter `OToggleGroup mobile-dropdown`, search with
a `max-md:min-w-*` floor, stat tiles via `OStatStrip` / `KpiCard`.
- [ ] **Rails and row actions adapt** — side rails render in an `ODrawer` anchored
to their trigger's row (or come from `OPageLayout #sidebar` / `FolderList`);
inline row actions are `max-md:hidden` with a `md:hidden` kebab mirroring them
(`<data-test>-menu`); no hover-only affordance without `max-md:opacity-100`.
- [ ] **Comments are one line, or none** — the *why* of a non-obvious constraint,
never layout narration ("< md this wraps"), a re-telling of the code, or the
history of the PR that added it (no ticket ids, "review finding", "as
discussed"). Same in specs. See
[conventions § Comments stay short](references/conventions.md).
- [ ] `cd web && npm run lint && npm run type-check:app` pass. **`type-check:app`,
not `type-check`** — the latter runs `tsconfig.vitest.json`, whose `include`
@@ -23,6 +23,7 @@ markup — don't guess a prop name.
| [references/page-recipes.md](page-recipes.md) | **Whole-page layouts: the listing/table page (full-height flush skeleton, mandatory search · refresh · column-visibility toggle, filtered-vs-first-run empty state) and the detail/editor page** |
| [references/navigation-menus.md](navigation-menus.md) | **Registering a new page in navigation: the route composables, the left-rail item (`linksList`), Settings/IAM sub-pages (`SectionRail`), hover-flyout children (`navGroups.ts`), and cloud/enterprise/RBAC gating** |
| [references/overlay-navigation.md](overlay-navigation.md) | Dialog, Drawer, Dropdown, Popover, Tooltip · Pagination, Stepper, Tabs |
| [references/responsive.md](responsive.md) | **Phones & tablets with an unchanged laptop view: breakpoints + `useBreakpoint`, one-row header/toolbar/stat chrome, rails → anchored drawers, row-action kebab, popup sizing, clipping and touch gotchas, verification** |
| [references/feedback-data.md](feedback-data.md) | Banner, Toast (+ useToast), Spinner, Skeleton, InnerLoading · ProgressBar, Timeline, Tree · FieldList |
> **Headless vs form-bound.** Most form controls come as a pair: `OInput`
@@ -54,6 +55,9 @@ markup — don't guess a prop name.
| Mutually exclusive choice | `ORadioGroup` (few) / `OSelect` (many) | forms-inputs |
| Instant on/off toggle | `OSwitch` | forms-inputs |
| Segmented control | `OToggleGroup` | core-controls-table |
| Filter strip that must fit a phone row | `OToggleGroup mobile-dropdown` | **responsive** |
| Side rail / field list on a phone | `ODrawer` with `anchor` (or `OPageLayout #sidebar` / `FolderList`) | **responsive** |
| Secondary header actions on a phone | `OPageHeader`/`OPageLayout` `#actions-overflow` | **responsive** |
| Build a validated form | `OForm` + Zod schema (binding rules) | **forms-validation** |
| Read form state for conditional rendering | `useOForm` (owner) / `inject`+`useStore` (child) | forms-validation |
| Add repeatable/dynamic field rows | `z.array` + indexed `name`, `:key="index"` | forms-validation |
@@ -148,8 +148,10 @@ conditional.
### Comments stay short
**One or two lines.** A comment carries the *why* behind a non-obvious choice —
not a re-telling of the code, and not the history of the PR that added it.
**One line, or none** (CLAUDE.md, enforced in review). A comment carries the *why*
behind a non-obvious constraint — not a re-telling of the code, not layout narration
("< md the toolbar wraps", "matches the header height"), and not the history of the
PR that added it. If the point needs a second line, it does not belong in a comment.
```vue
<!-- good -->
@@ -164,6 +164,7 @@ Authoring reference for O2's core control components (Button, Navbar, RefreshBut
- `disabled` (boolean, default `false`) — disables all items
- `label` (string) — text label beside the bar; for rich content use the `label` slot
- `labelPosition` (`"left"` | `"right"` | `"top"`, default `"left"`)
- `mobileDropdown` (boolean, default `false`) — below md a single-select horizontal group renders as a dropdown whose trigger shows the active item. Use it on every toolbar filter strip so it doesn't claim its own phone row. Data-tests: trigger `<group data-test>-dropdown-btn`, items `<item data-test>-item`. `AppTabs` forwards it. See [responsive § Toolbars](responsive.md#toolbars-one-row-filters-become-a-dropdown).
**Slots:** `default` (OToggleGroupItem children), `label` (overrides the `label` prop)
**Emits:** `update:modelValue` (`AcceptableValue | AcceptableValue[]`)
**Example:**
@@ -1,6 +1,6 @@
# House rules — full treatment (what · why · how)
> Extracted from the ui-architect contract. The always-loaded summary is in ../SKILL.md § The six house rules — read this for the full rationale and code.
> Extracted from the ui-architect contract. The always-loaded summary is in ../SKILL.md § The seven house rules — read this for the full rationale and code of rules 1–6; rule 7 (responsive) lives in [responsive.md](responsive.md).
## The house rules
@@ -87,6 +87,9 @@ Rule of thumb: **Tooltip = hint, Dropdown = action menu, Popover = anything free
- `seamless` (boolean — default `false`; hides the backdrop so the page stays interactive)
- `lazy` (boolean — default `true`; body slot mounts on open and is destroyed on close — set `false` to preserve child state across open/close)
- `portalTarget` (string selector | HTMLElement | null — scope the drawer to a container with `position:absolute`; the target needs `position:relative; overflow:hidden`)
- `anchor` (string selector | HTMLElement — the drawer and its backdrop start at this element's edge instead of the viewport top, so a mobile side panel opens from its trigger's row; re-measured on open and resize)
- `anchorEdge` (`"top"|"bottom"` — default `"top"`; use `"bottom"` when the trigger lives in the header itself)
- Below md a `width` is clamped to at least 88% so a "40%" drawer is still usable on a phone. See [responsive § Side panels](responsive.md#side-panels-open-from-their-own-row).
- Shared with ODialog: `open`, `persistent`, `title`, `subTitle`, `showClose`, `formId`, all `…ButtonLabel`/`…ButtonVariant`/`…ButtonDisabled`/`…ButtonLoading`
**Slots:** `trigger`, `header`, `header-left`, `header-right`, `default`, `footer` (identical to ODialog)
@@ -27,9 +27,11 @@ Any routed view is: an `OPageHeader` (rule 1) on top, then the page body below
it, all inside a **full-height flex column** so the header stays put and only the
body scrolls.
- **Primary page action** (New / Add / Create) and page-level secondary actions
(Import, an overflow `ODropdown`) go in the header's **`#actions`** slot as O2
buttons — never in the table toolbar.
- **Primary page action** (New / Add / Create) goes in the header's
**`#actions`** slot; page-level secondary actions (Import, Export, an overflow
`ODropdown`) go in **`#actions-overflow`** — inline on desktop, behind one ⋮ on
phones (add `overflow-first` so they keep their place before the CTA). Never in the
table toolbar. See [responsive.md](responsive.md#page-shell-the-header-stays-on-one-row).
- **The create CTA is label-only — no `icon-left="add"`.** Secondary header
actions keep their semantic icons (Import → `upload-file`), and in-section
adders inside the body keep their `+`; it is only the header's create button
@@ -141,21 +143,29 @@ for every list — don't ship a listing page without them.
data-test="channels-table"
@row-click="edit"
>
<!-- filters + search on the LEFT of the toolbar (fills the row) -->
<!-- filters + search on the LEFT of the toolbar (fills the row); max-md:contents +
mobile-dropdown keep the phone toolbar on one row -->
<template #toolbar>
<div class="flex items-center gap-2 w-full">
<OToggleGroup :model-value="typeFilter" @update:model-value="filterByType">
<div class="flex w-full min-w-0 items-center gap-2 max-md:contents">
<OToggleGroup
mobile-dropdown
:model-value="typeFilter"
data-test="channels-type-filter"
@update:model-value="filterByType"
>
<OToggleGroupItem value="all" size="sm">{{ t("channels.all") }}</OToggleGroupItem>
<OToggleGroupItem value="prebuilt" size="sm">{{ t("channels.prebuilt") }}</OToggleGroupItem>
<OToggleGroupItem value="custom" size="sm">{{ t("channels.custom") }}</OToggleGroupItem>
</OToggleGroup>
<OSearchInput
v-model="search"
class="flex-1"
:placeholder="t('channels.search')"
clearable
data-test="channels-search"
/>
<div class="min-w-0 flex-1 max-md:min-w-40">
<OSearchInput
v-model="search"
class="w-full"
:placeholder="t('channels.search')"
clearable
data-test="channels-search"
/>
</div>
</div>
</template>
@@ -352,7 +362,9 @@ visible data.
blank table.
- **Primary action** (New) is in the header `#actions`; **row actions**
(edit/delete/…) are in the `isAction` column as O2 buttons; **destructive
delete** goes through `ConfirmDialog` + `useConfirmDialog`.
delete** goes through `ConfirmDialog` + `useConfirmDialog`. On phones the inline
row buttons are `max-md:hidden` and one `md:hidden` kebab mirrors them — see
[responsive § Tables and row actions](responsive.md#tables-and-row-actions).
- **Keyboard shortcuts** (register + bind — see
[keyboard-shortcuts.md](keyboard-shortcuts.md)): `n` → create, `/` → focus
search, `r` → refresh. Advertise `r` via the refresh button's `OTooltip
@@ -403,7 +415,12 @@ form — those go in an `ODialog`/`ODrawer`; see SKILL.md § Forms):
- [ ] **`:forbidden`** bound to a ref the page sets from a 403 on *its own* list
request (cleared at the start of every load path), so a denied user sees
"You don't have access" rather than a create CTA that will 403.
- [ ] Row actions in an `isAction` column; delete via `ConfirmDialog`.
- [ ] Row actions in an `isAction` column; delete via `ConfirmDialog`; inline
buttons `max-md:hidden` + a `md:hidden` kebab mirroring them (`-menu` data-tests).
- [ ] **Responsive** — toolbar recipe (`max-md:contents`, `mobile-dropdown`, search
floor), footer count `max-md:hidden`, secondary header actions in
`#actions-overflow`; checked at 375 / 768 / 1280 with 1280 identical to main
(see [responsive.md](responsive.md)).
- [ ] `n` / `/` / `r` keyboard shortcuts registered and bound.
- [ ] **Registered in navigation** and gated for the right env/role — see
[navigation-menus.md](navigation-menus.md).
@@ -0,0 +1,276 @@
# Responsive layout — phones, tablets, and an untouched laptop
Every page in `web/` must work on a phone (375 / 360 px wide) and a tablet
(768 px) **without changing what a laptop shows**. This reference is the method the
whole app was converted with; new pages follow it from the first commit instead of
being retro-fitted.
## Table of contents
- [The contract](#the-contract)
- [Page shell: the header stays on one row](#page-shell-the-header-stays-on-one-row)
- [Side panels open from their own row](#side-panels-open-from-their-own-row)
- [Toolbars: one row, filters become a dropdown](#toolbars-one-row-filters-become-a-dropdown)
- [Tables and row actions](#tables-and-row-actions)
- [Stat strips and KPI cards: one row](#stat-strips-and-kpi-cards-one-row)
- [Popups, dropdowns and dialogs fit the viewport](#popups-dropdowns-and-dialogs-fit-the-viewport)
- [Nothing is clipped](#nothing-is-clipped)
- [Forms and settings rows](#forms-and-settings-rows)
- [Touch has no hover](#touch-has-no-hover)
- [Gotchas that already shipped once](#gotchas-that-already-shipped-once)
- [Verification](#verification)
---
## The contract
| Tier | Width | Tailwind | JS (`useBreakpoint()`) |
| --- | --- | --- | --- |
| Mobile | `< 48rem` (768 px) | `max-md:` | `isMobile` (`!mdUp`) |
| Tablet | `48rem – 64rem` | `md:max-lg:` | `isTablet` |
| Desktop / laptop | `≥ 64rem` (1024 px) | unprefixed | `isDesktop` (`lgUp`) |
1. **Desktop is frozen.** Every responsive rule is *additive below a breakpoint*:
`max-md:` / `max-lg:` / `md:max-lg:` variants, `@media (max-width: 47.99rem)` in a
surviving `<style>` block, or a JS branch on `isMobile` / `!lgUp`. An unprefixed
class change, a bare `md:`/`lg:` class, or a new always-on prop/attribute changes
the laptop view and is a review blocker — even when it "looks the same".
2. **CSS first, JS only for structure.** Hiding, wrapping, stacking and resizing are
Tailwind variants. Reach for `useBreakpoint()` (`@/composables/useBreakpoint`) only
when the *component tree* changes — an inline rail becomes a drawer, a toggle strip
becomes a dropdown, a splitter locks shut. It is a singleton (one `matchMedia` pair
for the app), so calling it per component is free.
3. **Reuse the library behaviour.** `OPageLayout`, `OPageHeader`, `OTable`,
`OToggleGroup`, `OStatStrip`, `KpiCard`, `FolderList`, `ODrawer` and every popup
already adapt. A new page that uses them correctly is mostly responsive for free;
hand-rolled equivalents are where phones break.
Audit the desktop surface of a change before calling it done:
```bash
git diff origin/main -- web/src | grep '^+' | grep -vE 'max-md:|max-lg:|md:max-lg:|@media \(max-width'
```
Every added line that still carries a class, prop or style is a potential laptop
change — justify it or gate it.
---
## Page shell: the header stays on one row
`OPageLayout` / `OPageHeader` handle the phone header: the decorative module icon and
the subtitle drop below md, the title truncates, and below lg the row may wrap
(desktop keeps its fixed `h-15` row so `h-full` slot content — inline tabs, dividers —
spans it).
- **Primary actions in `#actions`, secondary ones in `#actions-overflow`.** The
overflow slot renders inline on desktop and collapses behind one ⋮ "More" button
below md, so the primary CTA keeps the title row. Add **`overflow-first`** when the
secondaries sit *before* the primary on desktop (the usual "Import · Export · New"
order) — without it they render after `#actions`.
- **Never reorder the laptop toolbar by moving a control between slots.** If a
secondary control sits between primaries on desktop, render it twice:
`v-if="!isMobile"` at its desktop position in `#actions`, `v-if="isMobile"` in
`#actions-overflow` (see `ViewDashboard.vue`'s `AutoRefreshInterval`).
- Wide header controls (date pickers) drop their text label below md:
`class="max-md:[&_.date-time-label]:hidden"`; labelled buttons go icon-only with
`<span class="max-md:hidden">{{ label }}</span>` and keep a `:title` / `OTooltip`.
- A header should never take more than two rows on a phone. If it does, something
belongs in `#actions-overflow` or in the page body.
## Side panels open from their own row
Only the main navigation drawer slides in from the top of the screen. Every other
side panel opens **from the row its trigger lives in**, so the chrome above it stays
visible.
- **`OPageLayout #sidebar`** becomes a left `ODrawer` below md. Its ☰ trigger is
claimed by the page's first `OPageHeader` (no extra row) and the drawer anchors to
the header's bottom edge; it closes on navigation. Don't add your own trigger row.
Drawer content mounts lazily, so nothing the page needs on load may depend on a
sidebar component's mount side effects.
- **`FolderList`** collapses to a one-row folder trigger + drawer below md on its own.
Pass `:drawer-on-mobile="false"` when it is already inside an `OPageLayout`
`#sidebar` (a drawer inside a drawer).
- **Any other rail** (field list, facet rail, chat history, integration list):
hide the inline copy (`max-md:hidden` or `v-if="!isMobile"`) and render the same
component in a drawer anchored to its trigger's row:
```vue
<div data-drawer-anchor="alert-library-toolbar" class="flex items-center gap-3">
<OButton class="md:hidden" size="icon-toolbar" icon-left="filter-list"
data-test="alert-library-mobile-filters-btn" @click="filtersOpen = true" />
…
</div>
<ODrawer v-if="isMobile" v-model:open="filtersOpen" side="left" size="sm" bleed seamless
anchor='[data-drawer-anchor="alert-library-toolbar"]'>
<LibraryRail … />
</ODrawer>
```
`anchor` takes a CSS selector or an element; the drawer's top starts at the
anchor's top edge, or its bottom edge with `anchor-edge="bottom"` (use that when
the trigger sits in the header itself).
- **Splitters**: lock the rail pane shut on phones (`:limits="isMobile ? [0, 0] : limits"`
and set the model to 0), or flip `:horizontal="isMobile"` when both panes should
stack. Restore the desktop size when the breakpoint flips back.
- **A short rail** (Logs / Metrics / Traces, a handful of sections) reads better as a
horizontal tab strip than as a drawer: `DataSourceSidebarLayout compact-mode="strip"`,
or `OTabs :orientation="isMobile ? 'horizontal' : 'vertical'"`.
## Toolbars: one row, filters become a dropdown
The recipe every list toolbar follows:
```vue
<template #toolbar>
<!-- max-md:contents lets the filter, the search and OTable's own controls share one row -->
<div class="flex w-full min-w-0 items-center gap-2 max-md:contents">
<OToggleGroup v-model="typeFilter" mobile-dropdown data-test="channels-type-filter">
<OToggleGroupItem value="all" size="sm" data-test="channels-type-all">…</OToggleGroupItem>
…
</OToggleGroup>
<div class="min-w-0 flex-1 max-md:min-w-40">
<OSearchInput v-model="search" class="w-full" … />
</div>
</div>
</template>
```
- **`mobile-dropdown`** turns a single-select horizontal `OToggleGroup` into a
dropdown below md: the trigger shows the active item, the menu lists every item with
a check on the active one. Data-tests: trigger `<group data-test>-dropdown-btn`,
items `<item data-test>-item`. `AppTabs` forwards the same prop. This is the answer
whenever a filter strip would claim its own row — do not hand-roll a second control.
- **Search needs a floor.** `flex-1` is `basis-0`; without `max-md:min-w-40` (or
`min-w-24` in a crowded row) it shrinks to nothing instead of wrapping. A fixed
`w-64` search becomes `w-64 max-md:w-auto max-md:min-w-40 max-md:flex-1`.
- **Page toolbars** (Logs / Metrics / Traces style): the mode toggle becomes an
`ODropdown` showing the current mode (see `MetricsExplorer.vue`),
`AutoRefreshInterval :is-compact="isMobile"`, text buttons go icon-only, spacers
that pin a cluster right are `max-md:hidden` and the cluster takes `max-md:ms-auto`.
- An `OTable` toolbar wraps below md and right-aligns its own controls (column toggle,
`#toolbar-trailing`) automatically.
## Tables and row actions
- **`OTable` handles phones itself**: every column stays, the table scrolls
horizontally within its frame, oversized columns are capped at 200 px, the index
column is dropped and persisted desktop column sizes are ignored. Tablets get a
horizontal-scroll fallback once fill columns can't fit. **Do not hide data columns
for mobile** — there is no per-column breakpoint flag.
- **Row actions → one kebab below md.** Inline action buttons get
`class="max-md:hidden"`; add one `ODropdown` whose trigger is `md:hidden` and whose
items (each `class="md:hidden"`) mirror the inline buttons — same `v-if`, same
`:disabled`, `data-test="<inline data-test>-menu"`:
```vue
<OButton class="max-md:hidden" icon-left="edit" :data-test="`x-${row.id}-edit`" @click="edit(row)" />
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton icon-left="more-vert" variant="ghost" size="icon-xs-sq" class="md:hidden"
data-test="x-row-more-actions" @click.stop />
</template>
<ODropdownItem icon-left="edit" class="md:hidden" :data-test="`x-${row.id}-edit-menu`"
@select="edit(row)">
<span>{{ t("common.edit") }}</span>
</ODropdownItem>
</ODropdown>
```
If the row already has an overflow menu, prepend the mirrored items (plus an
`ODropdownSeparator class="md:hidden"`) instead of adding a second kebab. When every
item is conditional, `v-if` the whole `ODropdown` on "any item applies" so a row never
opens an empty menu. Hover-only buttons with no click action (a preview tooltip) have
no menu counterpart.
- **Footer on one row**: the plain "N items" count is `max-md:hidden` — the pager's
"x – y of z" already says it. Bulk-action buttons stay.
## Stat strips and KPI cards: one row
`OStatStrip` / `OStatCard` and `KpiCard` / `KpiCardRow` compact below lg to icon +
value; the label moves into the tile's `title`. Use them. A hand-rolled tile row follows
the same recipe so it never takes two rows of chrome:
- row: `max-lg:flex max-lg:flex-wrap max-lg:gap-1.5` (or `max-md:grid-cols-4` for a
fixed four-up);
- tile: `max-lg:shrink-0 max-lg:basis-auto max-lg:px-1.5 max-lg:py-1`, `:title="label"`;
- label, caption, trend and sparkline: `max-lg:hidden`; value: `max-lg:text-lg`.
## Popups, dropdowns and dialogs fit the viewport
- Library popups (`ODropdown`, `OPopover`, `OSelect`, `OCombobox`, `OColor`, `OTime`,
`ODateTimeRange`): below lg they cap to the available height, scroll, and keep an
8 px margin from the viewport edge. Build floating UI from them, not from absolutely
positioned divs.
- **No popup is wider than the phone.** A fixed width becomes
`w-156 max-lg:w-[calc(100vw-1.5rem)]`; an inline or constant width uses
`min(48rem, calc(100vw - 1.5rem))` (see `FIELD_FUNCTION_MENU_WIDTH`). Multi-pane
popups stack with `max-md:flex-col`.
- `ODrawer` percentage widths clamp to 88% on phones automatically.
- A hover-opened submenu inside a menu needs a tap path below md (see
[Touch has no hover](#touch-has-no-hover)).
## Nothing is clipped
- **`h-full` beside a stacked sibling** inside an `overflow-hidden` column is clipped
once the row becomes a column (`max-md:flex-col`): `h-full` beats `flex-1`. Give that
pane `max-md:h-auto max-md:min-h-0`.
- A fixed-height region whose content grows on phones scrolls: `max-lg:overflow-y-auto`,
and grids that must size to content use `max-lg:auto-rows-min`.
- `OEmptyState` already scrolls with safe centering below lg; custom hero content goes
in a `max-lg:overflow-y-auto` container.
- Full-viewport shells use `h-dvh`, never `100vh` (mobile browser chrome overlaps it).
- Long chips and IDs: `max-md:max-w-full max-md:truncate` on the text, not on the row.
## Forms and settings rows
- Side-by-side fields stack: `max-md:flex-col` / `max-md:grid-cols-1`; label/value
settings grids `grid-cols-3 max-lg:grid-cols-1`.
- Fixed field widths (`w-100`, `min-w-100`, inline `style="width: 30%"`) get
`max-md:w-full` / `max-md:min-w-0` (with `!` when an inline style must lose).
- Tall editors in a drawer or split view: stack panes below lg
(`:horizontal="!lgUp"` on the splitter) and give the empty preview pane no height
until it has something to show.
## Touch has no hover
- Hover-revealed affordances — `opacity-0 group-hover:opacity-100`,
`invisible group-hover:visible`, `hidden group-hover:flex` — get the matching
`max-md:opacity-100` / `max-md:visible` / `max-md:flex`.
- A tap fires `mouseenter` **before** `click`. A hover-opened submenu must ignore hover
below md (`@mouseenter="!isMobile && …"`) and toggle on click only, ignoring clicks
that land inside the submenu itself.
- Nav/flyout menus that open on hover become inline accordions below md (`ONavGroup`).
## Gotchas that already shipped once
- **`min-h-*` instead of `h-*` on a desktop row breaks `h-full` children.** Percentage
heights need a definite parent height; keep fixed heights on desktop and relax them
only below a breakpoint.
- **An empty flex item still costs a `gap`.** Render wrapper divs only when they have
content, or the siblings shift by the gap.
- **`chevron-down` is not in the icon registry** — it renders an empty box. Use
`arrow-drop-down` for dropdown carets.
- **Container width ≠ viewport width.** A toolbar squeezed by a folder rail at desktop
width uses a named container query (`@container/name` + `@max-[38rem]/name:hidden`),
not a viewport breakpoint.
- **Watchers that resize on the breakpoint** (`watch(isMobile, …, { immediate: true })`)
must restore the desktop value when flipping back and must not overwrite a user's
desktop choice on first load.
- **Compact labels must stay correct**: shortening a date range may drop the year only
when both ends share it.
## Verification
1. In the in-app browser pane (not Playwright) against the local dev server, check
**375×812, 360, 768×1024 and 1280** for every page you touched.
2. At 1280 compare against main side by side — header height, toolbar order, column
widths, popup positions. Identical is the bar.
3. Useful in-page probes (`javascript_tool`): elements with `overflow: hidden` whose
`scrollWidth/scrollHeight` exceed their client size (clipping); the number of distinct
row tops in the header + toolbar band before the content (chrome rows — aim for one,
two at most); popup `getBoundingClientRect()` against `innerWidth/innerHeight`.
4. `cd web && npm run lint && npm run type-check:app`, plus the specs of every library
component you changed. Specs run with a desktop `matchMedia` (min-width queries match),
so a mobile branch needs its own test that stubs `matchMedia` to not match.
@@ -4453,6 +4453,7 @@
"minuteWithValue": "1b1794a11c6d012e",
"minutes": "4f846a84e7fc9ef6",
"months": "09ca7551b3e90626",
"more": "d47d7cb0e4f8fd2b",
"move": "6ecc3df6bffdd06c",
"name": "dcd1d5223f73b3a9",
"nameIsRequired": "604fd1b2e29c688a",
@@ -4551,6 +4552,7 @@
"redirecting": "cd23991b4e02a17e"
},
"showMore": "ba8a80fdd7376631",
"sidePanel": "c28cdd98645b370f",
"sidebar": {
"folderCreateFailed": "4bb050aa86768e25",
"folderUpdateFailed": "ff2c3beb908479a5"
@@ -13016,6 +13018,7 @@
"quickPickStreamsLabel": "aa2db1e403a5ced8",
"range": "99379c2fee16dece",
"recordsChip": "d135619da331f2a4",
"recordsChipShort": "fd8f122cce20482f",
"recordsPerPage": "7051b8bbc62daa11",
"redirect_to_logs_page": "2445f40404b0508b",
"refreshCacheAndRunQuery": "e90aca13f125a692",
@@ -13059,6 +13062,7 @@
"sessionRefreshedRetrySearch": "a5f9f50b88e0d55d",
"shareLink": "6480bb463dff4777",
"showEditor": "a314f8725f27e525",
"showFields": "c47c7b14bdf3c94b",
"showHideColumns": "0b71f0103a1f85d5",
"showHistogramLabel": "b32dc489fba87a9a",
"showOnlyInterestingFields": "fb62632df8bc2208",
+38 -7
View File
@@ -35,13 +35,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:class="{
[selectedType + 'type']: !disableRelative,
hideRelative: disableRelative,
'min-w-71.5': !disableRelative && selectedType === 'absolute',
'md:min-w-71.5': !disableRelative && selectedType === 'absolute',
'w-fit': disableRelative,
}"
class="max-md:max-w-full max-md:min-w-0"
:disabled="disable"
icon-left="schedule"
>
<span class="date-time-label flex-1 text-left font-semibold">{{ triggerLabel }}</span>
<span
class="date-time-label flex-1 text-left font-semibold max-md:min-w-0 max-md:truncate"
>{{ triggerLabel }}</span
>
<template #icon-right
><OIcon
name="arrow-drop-down"
@@ -316,6 +320,7 @@ import { copyToClipboard } from "@/utils/clipboard";
import { toast } from "@/lib/feedback/Toast/useToast";
import { useStore } from "vuex";
import { raw, useI18nTyped, type I18nKey } from "@/types/i18n";
import useBreakpoint from "@/composables/useBreakpoint";
import { toZonedTime, fromZonedTime } from "date-fns-tz";
interface ConsumableDateTime {
@@ -966,6 +971,30 @@ export default defineComponent({
markApplied();
};
const { isMobile } = useBreakpoint();
const compactRangeLabel = (label: string) => {
const REL_UNIT: Record<string, string> = {
second: "s",
minute: "m",
hour: "h",
day: "d",
week: "w",
month: "M",
};
const rel = label.match(/^Past (\d+) ([A-Za-z]+?)s?$/);
if (rel) return `Past ${rel[1]}${REL_UNIT[rel[2].toLowerCase()] ?? ` ${rel[2]}`}`;
const abs = label.match(
/^(\d{4})\/(\d{2}\/\d{2}) (\d{2}:\d{2})(?::\d{2})? - (\d{4})\/(\d{2}\/\d{2}) (\d{2}:\d{2})(?::\d{2})?$/,
);
// The year is only dropped when both ends share it, or a cross-year range would read as same-day.
if (!abs || abs[1] !== abs[4]) return label.replace(/\b(\d{2}:\d{2}):\d{2}\b/g, "$1");
const [, , fromDay, fromTime, , toDay, toTime] = abs;
return fromDay === toDay
? `${fromDay} ${fromTime} - ${toTime}`
: `${fromDay} ${fromTime} - ${toDay} ${toTime}`;
};
/**
* What the trigger button renders.
*
@@ -976,11 +1005,13 @@ export default defineComponent({
* back to what's actually applied; see `appliedDisplayValue`.
* The `||` fallback covers the first paint, before the mount-time apply.
*/
const triggerLabel = computed(() =>
props.autoApply || menuOpen.value
? getDisplayValue.value
: appliedDisplayValue.value || getDisplayValue.value,
);
const triggerLabel = computed(() => {
const label =
props.autoApply || menuOpen.value
? getDisplayValue.value
: appliedDisplayValue.value || getDisplayValue.value;
return isMobile.value ? compactRangeLabel(label) : label;
});
const getDisplayValue = computed(() => {
if (!props.disableRelative && selectedType.value === "relative") {
+24 -6
View File
@@ -17,7 +17,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="bg-surface-chrome-deeper flex h-10 w-full shrink-0 flex-nowrap items-center">
<!-- LEFT SIDE: Logo -->
<div class="flex shrink-0 items-center justify-start ps-3">
<div class="flex shrink-0 items-center justify-start gap-1 ps-3 max-md:ps-1.5">
<OButton
variant="ghost"
size="icon-toolbar"
class="md:hidden"
data-test="header-mobile-nav-toggle"
:aria-label="t('components.navbar.mainNavigation')"
@click="$emit('toggle-mobile-nav')"
>
<OIcon name="menu" size="sm" class="size-5!" />
</OButton>
<!-- LOGO SECTION: Displays custom or default OpenObserve logo -->
<!-- Shows custom logo/text if configured in enterprise mode -->
<div
@@ -136,7 +146,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="flex shrink-0 items-center justify-end gap-1 pe-3">
<!-- QUOTA WARNING SECTION: Shows warning when quota threshold is reached -->
<div
class="me-4 flex items-center gap-1"
class="me-4 flex items-center gap-1 max-md:hidden"
v-if="store.state.organizationData.quotaThresholdMsg"
>
<div type="warning" icon="cloud" class="bg-status-warning-bg rounded-default inline p-1.25">
@@ -186,6 +196,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
: 'primary'
"
size="xs"
class="max-md:hidden"
data-test="upgrade-to-enterprise-btn"
@click="openEnterpriseDialog"
>
@@ -238,12 +249,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<!-- THEME SWITCHER: Toggle between light and dark mode -->
<ThemeSwitcher></ThemeSwitcher>
<ThemeSwitcher class="max-md:hidden"></ThemeSwitcher>
<!-- SLACK COMMUNITY LINK -->
<OButton
variant="ghost"
size="icon-toolbar"
class="max-md:hidden"
data-test="menu-link-slack-item"
@click="openSlack"
>
@@ -254,7 +266,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- HELP MENU: Contains links to docs, API, and about page -->
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton variant="ghost" size="icon-toolbar" data-test="menu-link-help-item">
<OButton
variant="ghost"
size="icon-toolbar"
class="max-md:hidden"
data-test="menu-link-help-item"
>
<OIcon name="help-outline" size="sm" class="size-5!" />
<OTooltip side="top" align="center" :content="t('menu.help')" />
</OButton>
@@ -336,7 +353,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Language selector — nested sub-dropdown (click to open) -->
<div
data-test="header-language-submenu-trigger"
class="hover:bg-dropdown-item-hover-bg relative flex cursor-pointer items-center gap-3 px-3 py-1.5 text-sm leading-[1.2] select-none"
class="hover:bg-dropdown-item-hover-bg relative flex cursor-pointer items-center gap-3 px-3 py-1.5 text-sm leading-[1.2] select-none max-md:flex-wrap"
@click.stop="showLanguageSubmenu = !showLanguageSubmenu"
>
<OIcon size="xs" name="language" class="padding-none" />
@@ -361,7 +378,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Submenu — absolutely positioned to the left of parent dropdown -->
<div
v-if="showLanguageSubmenu"
class="rounded-default bg-dropdown-bg border-dropdown-border absolute end-full top-0 z-9999 me-1 min-w-50 border py-1 shadow-lg dark:shadow-lg"
class="rounded-default bg-dropdown-bg border-dropdown-border absolute end-full top-0 z-9999 me-1 min-w-50 border py-1 shadow-lg max-md:static max-md:me-0 max-md:mt-1 max-md:w-full max-md:min-w-0 max-md:basis-full max-md:shadow-none dark:shadow-lg"
data-test="language-dropdown-item"
@click.stop
>
@@ -542,6 +559,7 @@ export default defineComponent({
"changeLanguage",
"openPredefinedThemes",
"openShortcuts",
"toggle-mobile-nav",
"signout",
],
setup(props, { emit }) {
+12 -7
View File
@@ -19,7 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
and submenu-group triggers (<button>, used by ONavGroup) all share the
exact same tile markup and styling — a group tile is literally a MenuLink. -->
<component :is="rootComponent" v-bind="rootProps" :class="rootClass" @click="onRootClick">
<div class="nav-menu-item-avatar flex w-full flex-col items-center gap-0.5">
<div
class="nav-menu-item-avatar flex w-full flex-col items-center gap-0.5 max-md:flex-row max-md:items-center max-md:gap-3 max-md:px-1"
>
<div
class="icon-wrapper rounded-default relative inline-flex items-center justify-center p-0.5 transition-colors duration-250"
:class="isActive ? activeIconClass : 'text-tabs-inactive-text group-hover:text-accent'"
@@ -37,7 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<div
class="nav-menu-item-label line-clamp-2 w-full text-center text-xs leading-tight tracking-[0.01em] break-normal wrap-normal [hyphens:none] transition-colors duration-250"
class="nav-menu-item-label line-clamp-2 w-full text-center text-xs leading-tight tracking-[0.01em] break-normal wrap-normal [hyphens:none] transition-colors duration-250 max-md:text-left max-md:text-sm"
:class="
isActive
? activeLabelClass
@@ -53,15 +55,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
hover, or stays lit while the flyout is open / the section is active. -->
<span
v-if="asTrigger || submenu"
class="absolute top-3 right-1 transition-opacity duration-150"
:class="
class="absolute top-3 right-1 transition-opacity duration-150 max-md:top-1/2 max-md:right-3 max-md:-translate-y-1/2 max-md:opacity-100"
:class="[
isActive || expanded
? 'text-accent opacity-100'
: 'text-tabs-inactive-text opacity-70 group-hover:opacity-100'
"
: 'text-tabs-inactive-text opacity-70 group-hover:opacity-100',
expanded ? 'max-md:rotate-90' : '',
]"
aria-hidden="true"
>
<OIcon name="chevron-right" size="xs" />
<OIcon name="chevron-right" size="xs" class="max-md:size-4.5!" />
</span>
</component>
</template>
@@ -267,6 +270,8 @@ export default defineComponent({
const rootClass = computed(() => [
"nav-menu-item",
"group relative block [text-decoration:none]! text-inherit shrink-0 mx-1 px-0 py-1 min-h-0 rounded-surface transition-colors duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1",
// Drawer rows are tap targets, so they clear the 44px touch-target minimum.
"max-md:flex max-md:min-h-11 max-md:items-center max-md:px-2 max-md:py-2",
// Sit above the rail's sliding pill so icon/label stay readable.
slideActive.value ? "z-10" : "",
isActive.value
+1 -1
View File
@@ -65,7 +65,7 @@
<OButton
variant="ghost"
size="icon-xs-circle"
class="delete-history-btn opacity-0 transition-opacity duration-200 group-hover:opacity-100"
class="delete-history-btn opacity-0 transition-opacity duration-200 group-hover:opacity-100 max-md:opacity-100"
@click.stop="deleteChat(chat.id)"
>
<OIcon name="delete" size="sm" />
+1 -1
View File
@@ -206,7 +206,7 @@ const rowStateClass = (row: { org: OrgOption; index: number }) => {
variant="outline-primary"
size="xs"
data-test="navbar-organizations-select-trigger"
class="text-text-body! w-56"
class="text-text-body! w-56 max-md:w-28"
:class="open ? 'ring-focus-ring-accent ring-2 ring-inset' : ''"
>
<template #icon-left>
@@ -48,7 +48,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<!-- ── Edition Cards Grid ──────────────────────────────────────────── -->
<div class="grid grid-cols-3 gap-5 pt-4">
<div class="grid grid-cols-3 gap-5 pt-4 max-md:grid-cols-1">
<!-- eslint-disable local/no-hardcoded-px -- query condition: a threshold for WHEN layout changes, not a rendered length -->
<div
v-for="ed in editionList"
+10 -4
View File
@@ -159,7 +159,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</div>
<div class="flex min-h-0 flex-1">
<div class="flex min-h-0 flex-1 max-lg:flex-col">
<!-- LEFT column wrapper (flex: 6.5) -->
<div
:class="[
@@ -198,7 +198,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OToggleGroupItem>
</OToggleGroup>
</div>
<div v-if="!isCompositeMode" class="flex items-center gap-4 px-3 py-2">
<div
v-if="!isCompositeMode"
class="flex items-center gap-4 px-3 py-2 max-md:flex-wrap max-md:gap-2"
>
<!-- Stream Type -->
<div v-if="!isCompositeMode" class="flex items-center gap-1.5">
<div class="text-text-heading text-xs font-semibold whitespace-nowrap">
@@ -219,7 +222,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<!-- Stream Name -->
<div v-if="!isCompositeMode" class="flex min-w-0 flex-1 items-center gap-1.5">
<div
v-if="!isCompositeMode"
class="flex min-w-0 flex-1 items-center gap-1.5 max-md:basis-full"
>
<div class="text-text-heading text-xs font-semibold whitespace-nowrap">
{{ t("alerts.stream_name") }} <span class="text-text-body">*</span>
</div>
@@ -456,7 +462,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- border-s: full-height vertical divider flush against the Preview/Summary pane -->
<div
v-if="!isCompositeMode"
class="border-border-default flex min-h-0 min-w-0 flex-[3.5] flex-col gap-2 overflow-hidden border-s pt-2 pb-2"
class="border-border-default flex min-h-0 min-w-0 flex-[3.5] flex-col gap-2 overflow-hidden border-s pt-2 pb-2 max-lg:border-s-0 max-lg:border-t"
>
<!-- Preview Card -->
<div class="bg-card-glass-bg flex min-h-0 flex-1 flex-col overflow-hidden">
+3 -1
View File
@@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}"
bleed
>
<OSplitter class="h-full" v-model="splitterModel" unit="%" :horizontal="false">
<OSplitter class="h-full" v-model="splitterModel" unit="%" :horizontal="isMobile">
<template v-slot:before>
<OForm :form="form" v-slot="{ isSubmitting }" class="bg-card-glass-bg flex h-full flex-col">
<div class="overflow-auto p-3">
@@ -314,6 +314,7 @@ import {
type AddTemplateForm,
} from "./AddTemplate.schema";
import ContentTemplateForm from "./template-content/ContentTemplateForm.vue";
import useBreakpoint from "@/composables/useBreakpoint";
import {
emptyContentSpec,
linkUrlBadScheme,
@@ -341,6 +342,7 @@ const emit = defineEmits(["get:templates", "cancel:hideform"]);
const QueryEditor = defineAsyncComponent(() => import("@/components/CodeQueryEditor.vue"));
const { t } = useI18nTyped();
const splitterModel: Ref<number> = ref(55);
const { isMobile } = useBreakpoint();
const store = useStore();
const router = useRouter();
const isUpdatingTemplate = ref(false);
@@ -42,7 +42,7 @@
data-test="alerts-alertgroupstable-table"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="search"
class="flex-1"
+73 -1
View File
@@ -43,7 +43,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:model-value="onAlertSelected"
:placeholder="t('alerts.searcHistory')"
data-test="alert-history-search-select"
class="o2-search-input min-w-62.5"
class="o2-search-input min-w-62.5 max-md:hidden"
clearable
@clear="clearSearch"
>
@@ -60,6 +60,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
icon-left="search"
size="icon-sm"
class="max-md:hidden"
@click="manualSearch"
data-test="alert-history-manual-search-btn"
:disabled="loading"
@@ -77,6 +78,41 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OTooltip :content="t('common.refresh')" />
</OButton>
</template>
<div
class="border-border-default flex shrink-0 items-center gap-1 border-b px-3 py-1.5 md:hidden"
>
<OSelect
v-model="selectedAlert"
:options="filteredAlertOptions"
labelKey="label"
valueKey="value"
@update:model-value="onAlertSelected"
:placeholder="t('alerts.searcHistory')"
data-test="alert-history-search-select-mobile"
class="o2-search-input min-w-0 flex-1"
clearable
@clear="clearSearch"
>
<template #icon-left>
<OIcon class="o2-search-input-icon" name="search" size="sm" />
</template>
<template #empty>
<div class="text-muted-foreground px-3 py-2">
{{ t("alerts.noAlertsFound") }}
</div>
</template>
</OSelect>
<OButton
variant="ghost"
icon-left="search"
size="icon-sm"
@click="manualSearch"
data-test="alert-history-manual-search-btn-mobile"
:disabled="loading"
>
<OTooltip :content="t('common.search')" />
</OButton>
</div>
<div class="min-h-0 flex-1 overflow-hidden">
<div class="bg-card-glass-bg h-full">
<!-- eslint-disable local/no-hardcoded-px -- mixed with vh/vw — vh tracks the window while rem tracks font-size; keep the expression unit-consistent -->
@@ -238,6 +274,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
icon-left="visibility"
class="max-md:hidden"
@click="showDetailsDialog(row)"
data-test="alert-history-view-details"
>
@@ -249,6 +286,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost-destructive"
size="icon-sm"
icon-left="error"
class="max-md:hidden"
@click.stop="showErrorDialog(row)"
>
<OTooltip
@@ -257,6 +295,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"
/>
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="alert-history-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="visibility"
class="md:hidden"
data-test="alert-history-view-details-menu"
@select="showDetailsDialog(row)"
>
<span>{{ t("alerts.viewDetails") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="row.error"
icon-left="error"
variant="destructive"
class="md:hidden"
:data-test="`pipeline-list-${row.name}-error-indicator-menu`"
@select="showErrorDialog(row)"
>
<span>{{
t("common.lastErrorAt", { time: new Date(row.timestamp / 1000).toLocaleString() })
}}</span>
</ODropdownItem>
</ODropdown>
</template>
</OTable>
</div>
@@ -486,6 +556,8 @@ import OButton from "@/lib/core/Button/OButton.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import { toast } from "@/lib/feedback/Toast/useToast";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
import { COL } from "@/lib/core/Table/OTable.types";
@@ -240,7 +240,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #bottom>
<div class="flex h-12 w-full items-center">
<div class="flex w-55 items-center text-xs font-normal">
<div class="flex w-55 items-center text-xs font-normal max-md:hidden">
{{ resultTotal }} {{ t("alerts.alertDetails.results") }}
</div>
</div>
+2 -2
View File
@@ -197,8 +197,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<!-- Dashboard Content -->
<div class="min-h-0 flex-1 px-2.5 pb-2.5">
<div class="bg-card-glass-bg mb-2.5 h-[calc(100vh-13rem)]">
<div class="min-h-0 flex-1 px-2.5 pb-2.5 max-lg:overflow-y-auto">
<div class="bg-card-glass-bg mb-2.5 h-[calc(100vh-13rem)] max-md:h-auto">
<div @contextmenu="handleNativeContextMenu">
<div v-show="isLoading" class="flex h-100 items-center justify-center">
<OSpinner size="md" />
+85 -30
View File
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div data-test="alert-list-page" class="flex h-full flex-col">
<OPageLayout
overflow-first
bleed
v-if="!showAddAlertDialog && !showImportAlertDialog"
:title="t('alerts.header')"
@@ -37,24 +38,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #actions>
<!-- The provider behind the Terraform export tab, which is otherwise
only discoverable once the export dialog is already open. -->
<IacRegistryLinks data-test="alert-list-iac-registries" />
<!-- Import button -->
<OButton
:class="isCompactToolbar ? 'min-w-0! px-2! py-0!' : ''"
variant="outline"
size="sm"
@click="importAlert"
data-test="alert-import"
icon-left="upload-file"
>
<template v-if="!isCompactToolbar">{{ t(`dashboard.import`) }}</template>
<OTooltip
v-if="isCompactToolbar"
:content="t('dashboard.import')"
side="bottom"
shortcut-id="alertsImport"
/>
</OButton>
<!-- Add button — routes to anomaly creation on anomaly tab, alert creation otherwise -->
<OButton
data-test="alert-list-add-alert-btn"
@@ -78,15 +61,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
</template>
<div data-test="alert-list-splitter" class="flex min-h-0 flex-1">
<template #actions-overflow>
<IacRegistryLinks data-test="alert-list-iac-registries" />
<!-- Import button -->
<OButton
:class="isCompactToolbar ? 'min-w-0! px-2! py-0!' : ''"
variant="outline"
size="sm"
@click="importAlert"
data-test="alert-import"
icon-left="upload-file"
>
<template v-if="!isCompactToolbar">{{ t(`dashboard.import`) }}</template>
<OTooltip
v-if="isCompactToolbar"
:content="t('dashboard.import')"
side="bottom"
shortcut-id="alertsImport"
/>
</OButton>
</template>
<div data-test="alert-list-splitter" class="flex min-h-0 flex-1 max-md:flex-col">
<!-- Left: FolderList -->
<div class="w-rail h-full shrink-0">
<div
class="w-rail max-md:border-border-default h-full shrink-0 max-md:h-auto max-md:w-full max-md:border-b"
>
<div class="h-full">
<FolderList type="alerts" @update:activeFolderId="updateActiveFolderId" />
</div>
</div>
<!-- Right: Table -->
<div class="h-full min-w-0 flex-1">
<div class="h-full min-w-0 flex-1 max-md:h-auto max-md:min-h-0">
<div class="bg-card-glass-bg flex h-full flex-col">
<!-- Alert List Table (shows all alert types including anomaly detection rows) -->
<OTable
@@ -139,8 +145,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Toolbar: alert-type filter + search (inline folder scope) + refresh. -->
<template #toolbar>
<div class="flex w-full items-center gap-2">
<!-- A container, not a viewport breakpoint: the folder rail squeezes this toolbar at any width. -->
<div
class="flex w-full items-center gap-2 max-lg:@container/alert-toolbar max-lg:min-w-0 max-lg:flex-1 max-lg:flex-wrap max-lg:gap-y-1.5 max-md:contents"
>
<OToggleGroup
mobile-dropdown
:model-value="activeTab"
data-test="alert-list-tabs"
@update:model-value="(v) => onAlertTabChange(v as string)"
@@ -151,12 +161,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:value="tab.value"
size="sm"
:icon-left="tab.icon"
:title="lgUp ? undefined : tab.label"
:data-test="`alert-list-tab-${tab.value}`"
>
{{ tab.label }}
<span class="@max-[34rem]/alert-toolbar:hidden">{{ tab.label }}</span>
</OToggleGroupItem>
</OToggleGroup>
<div class="min-w-0 flex-1">
<!-- flex-1 is basis-0, so the min-w floor is what wraps the input before its scope chips spill. -->
<div class="min-w-0 flex-1 md:max-lg:min-w-80">
<OInput
v-model="dynamicQueryModel"
:placeholder="
@@ -183,7 +195,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="folder-outline"
data-test="alert-list-search-scope-current"
:title="t('alerts.searchThisFolderTooltip')"
>{{ t("alerts.searchThisFolder") }}</OToggleGroupItem
><span class="max-md:hidden">{{
t("alerts.searchThisFolder")
}}</span></OToggleGroupItem
>
<OToggleGroupItem
value="all"
@@ -191,7 +205,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="search"
data-test="alert-list-search-across-folders-toggle"
:title="t('alerts.searchAllFoldersTooltip')"
>{{ t("alerts.searchAllFolders") }}</OToggleGroupItem
><span class="max-md:hidden">{{
t("alerts.searchAllFolders")
}}</span></OToggleGroupItem
>
</OToggleGroup>
</template>
@@ -476,7 +492,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="alert-list-loading-alert"
v-if="alertStateLoadingMap[row.uuid]"
style="display: inline-block; width: 2.07125rem; height: auto"
class="ms-1 flex items-center justify-center"
class="ms-1 flex items-center justify-center max-md:hidden"
:title="row.enabled ? t('common.turningOff') : t('common.turningOn')"
>
<OSpinner size="xs" />
@@ -485,7 +501,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-else
:data-row-action="row.enabled ? 'pause' : 'resume'"
:data-test="`alert-list-${row.name}-pause-start-alert`"
class="ms-1"
class="ms-1 max-md:hidden"
:variant="row.enabled ? 'ghost-destructive' : 'ghost-success'"
size="icon-sm"
:icon-left="row.enabled ? 'pause' : 'play-arrow'"
@@ -503,6 +519,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
icon-left="edit"
class="max-md:hidden"
@click.stop="editAlert(row)"
>
<OTooltip
@@ -516,7 +533,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
button receives no pointer events, so a tooltip anchored
to it would never open — leaving a greyed-out control
with no way to find out why. -->
<span class="inline-flex">
<span class="inline-flex max-md:hidden">
<OTooltip
v-if="isSloRow(row)"
side="bottom"
@@ -569,6 +586,39 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="`alert-list-${row.name}-more-options`"
/>
</template>
<ODropdownItem
class="md:hidden"
:disabled="!!alertStateLoadingMap[row.uuid]"
:data-test="`alert-list-${row.name}-pause-start-alert-menu`"
@select="toggleAlertState(row)"
>
<template #icon-left>
<OIcon :name="row.enabled ? 'pause' : 'play-arrow'" size="sm" />
</template>
{{ row.enabled ? t("alerts.pause") : t("alerts.start") }}
</ODropdownItem>
<ODropdownItem
class="md:hidden"
:data-test="`alert-list-${row.name}-update-alert-menu`"
@select="editAlert(row)"
>
<template #icon-left>
<OIcon name="edit" size="sm" />
</template>
{{ t("alerts.edit") }}
</ODropdownItem>
<ODropdownItem
class="md:hidden"
:disabled="isSloRow(row)"
:data-test="`alert-list-${row.name}-clone-alert-menu`"
@select="duplicateAlert(row)"
>
<template #icon-left>
<OIcon name="content-copy" size="sm" />
</template>
{{ t("alerts.clone") }}
</ODropdownItem>
<ODropdownSeparator class="md:hidden" />
<ODropdownItem
:data-test="`alert-list-${row.name}-move-alert`"
@select="moveAlertToAnotherFolder(row)"
@@ -676,7 +726,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>{{ selectedAlerts.length }} {{ t("alerts.conditionOf") }} {{ resultTotal }}
{{ t("alerts.selectedLabel") }}</template
>
<template v-else>{{ resultTotal }} {{ t("alerts.header") }}</template>
<span v-else class="max-md:hidden"
>{{ resultTotal }} {{ t("alerts.header") }}</span
>
</div>
<OButton
@@ -902,6 +954,7 @@ import FolderList from "../common/sidebar/FolderList.vue";
import MoveAcrossFolders from "../common/sidebar/MoveAcrossFolders.vue";
import { invalidateDependencyGraphCache } from "@/composables/alerts/useDependencyGraph";
import { nextTick } from "vue";
import useBreakpoint from "@/composables/useBreakpoint";
import SelectFolderDropDown from "../common/sidebar/SelectFolderDropDown.vue";
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
@@ -971,6 +1024,7 @@ export default defineComponent({
setup() {
const store = useStore();
const { t } = useI18nTyped();
const { lgUp } = useBreakpoint();
const router = useRouter();
const { track } = useReo();
const formData: Ref<Alert | {}> = ref({});
@@ -3418,6 +3472,7 @@ export default defineComponent({
]);
return {
lgUp,
raw,
t,
store,
@@ -38,7 +38,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="`alert-section-tab-${section.key}`"
>
<OIcon :name="section.icon" size="sm" class="shrink-0" />
<span>{{ section.label }}</span>
<span :class="section.key === activeSectionKey ? '' : 'max-md:hidden'">{{
section.label
}}</span>
</OTab>
</OTabs>
</template>
@@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="flex h-full flex-col p-0">
<OPageLayout
overflow-first
bleed
v-if="!showDestinationEditor && !showImportDestination"
:title="t('alerts.header')"
@@ -30,13 +31,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<template #actions>
<OButton
variant="outline"
size="sm"
@click="importDestination"
data-test="destination-import"
>{{ t(`dashboard.import`) }}</OButton
>
<OButton
data-test="alert-destination-list-add-alert-btn"
variant="primary"
@@ -46,6 +40,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>{{ t(`alert_destinations.add`) }}</OButton
>
</template>
<template #actions-overflow>
<OButton
variant="outline"
size="sm"
@click="importDestination"
data-test="destination-import"
>{{ t(`dashboard.import`) }}</OButton
>
</template>
<div class="bg-card-glass-bg min-h-0 flex-1">
<OTable
ref="oTableRef"
@@ -73,8 +77,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:current-page="onPageChange"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OToggleGroup
mobile-dropdown
:model-value="activeTab"
@update:model-value="
(v) => {
@@ -99,7 +104,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSearchInput
v-model="filterQuery"
data-test="destination-list-search-input"
class="flex-1"
class="flex-1 max-lg:min-w-0 max-md:min-w-40"
:placeholder="t('alert_destinations.search')"
/>
</div>
@@ -122,7 +127,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<template #bottom="{ totalRows }">
<span class="text-xs font-normal">
<span class="text-xs font-normal max-md:hidden">
{{ totalRows.toLocaleString() }} {{ t("alert_destinations.header") }}
</span>
<OButton
@@ -222,6 +227,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="export"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('alert_destinations.exportDestination')"
@click.stop="exportDestination(row)"
>
@@ -232,6 +238,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="edit"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('alert_destinations.edit')"
@click="editDestination(row)"
>
@@ -242,12 +249,50 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="delete"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('alert_destinations.delete')"
:loading="deletingDestinations.has(row.name)"
@click="conformDeleteDestination(row)"
>
<OIcon name="delete" size="sm" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="alert-destination-list-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="download"
class="md:hidden"
data-test="destination-export-menu"
@select="exportDestination(row)"
>
<span>{{ t("alert_destinations.exportDestination") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`alert-destination-list-${row.name}-update-destination-menu`"
@select="editDestination(row)"
>
<span>{{ t("alert_destinations.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`alert-destination-list-${row.name}-delete-destination-menu`"
@select="conformDeleteDestination(row)"
>
<span>{{ t("alert_destinations.delete") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
@@ -323,6 +368,8 @@ import { useReo } from "@/services/reodotdev_analytics";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import OTable from "@/lib/core/Table/OTable.vue";
@@ -350,6 +397,8 @@ export default defineComponent({
ImportDestination,
OButton,
OTooltip,
ODropdown,
ODropdownItem,
OSearchInput,
OTag,
OTable,
@@ -58,7 +58,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div
v-if="!deleting"
class="flex shrink-0 items-center gap-0.5 opacity-0 group-hover:opacity-100"
class="flex shrink-0 items-center gap-0.5 opacity-0 group-hover:opacity-100 max-md:opacity-100"
>
<OButton
v-if="!node.missing"
@@ -201,6 +201,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
icon-left="edit"
class="max-md:hidden"
:title="t('alert_sources.edit')"
:data-test="`alert-sources-edit-${row.integration.id}`"
@click="openEditFor(row.integration)"
@@ -209,6 +210,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
icon-left="autorenew"
class="max-md:hidden"
:title="t('alert_sources.rotateToken')"
:data-test="`alert-sources-rotate-${row.integration.id}`"
@click="confirmRotate(row.integration)"
@@ -217,6 +219,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:variant="row.integration.enabled ? 'ghost-destructive' : 'ghost-success'"
size="icon-sm"
:icon-left="row.integration.enabled ? 'pause' : 'play-arrow'"
class="max-md:hidden"
:title="
row.integration.enabled ? t('alert_sources.disable') : t('alert_sources.enable')
"
@@ -227,6 +230,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost-destructive"
size="icon-sm"
icon-left="delete"
class="max-md:hidden"
:disabled="row.integration.name === 'default'"
:title="
row.integration.name === 'default'
@@ -236,6 +240,58 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="`alert-sources-delete-${row.integration.id}`"
@click="confirmDelete(row.integration)"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="alert-sources-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`alert-sources-edit-${row.integration.id}-menu`"
@select="openEditFor(row.integration)"
>
<span>{{ t("alert_sources.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="autorenew"
class="md:hidden"
:data-test="`alert-sources-rotate-${row.integration.id}-menu`"
@select="confirmRotate(row.integration)"
>
<span>{{ t("alert_sources.rotateToken") }}</span>
</ODropdownItem>
<ODropdownItem
:icon-left="row.integration.enabled ? 'pause' : 'play-arrow'"
class="md:hidden"
:data-test="`alert-sources-toggle-enabled-${row.integration.id}-menu`"
@select="toggleEnabledFor(row.integration)"
>
<span>{{
row.integration.enabled ? t("alert_sources.disable") : t("alert_sources.enable")
}}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:disabled="row.integration.name === 'default'"
:data-test="`alert-sources-delete-${row.integration.id}-menu`"
@select="confirmDelete(row.integration)"
>
<span>{{
row.integration.name === "default"
? t("alert_sources.defaultCannotDelete")
: t("alert_sources.delete")
}}</span>
</ODropdownItem>
</ODropdown>
</div>
<span
v-else
@@ -285,6 +341,8 @@ import OIcon from "@/lib/core/Icon/OIcon.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import AddExternalAlertSource from "./AddExternalAlertSource.vue";
import alertSources from "@/services/alert_sources";
@@ -332,6 +390,8 @@ export default defineComponent({
OSearchInput,
OEmptyState,
OTooltip,
ODropdown,
ODropdownItem,
ConfirmDialog,
AddExternalAlertSource,
},
+9 -7
View File
@@ -27,8 +27,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<!-- Custom URL Input Section with Folder Dropdown -->
<template #url-input-section="{ url, updateUrl }">
<div class="my-[0.725rem] flex items-end gap-2">
<div class="w-[69%]">
<div class="my-[0.725rem] flex items-end gap-2 max-md:flex-wrap">
<div class="w-[69%] max-md:w-full">
<OInput
data-test="alert-import-url-input"
:model-value="url"
@@ -38,7 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</div>
<div class="w-[30%]" data-test="alert-folder-dropdown">
<div class="w-[30%] max-md:w-full" data-test="alert-folder-dropdown">
<SelectFolderDropDown
:type="'alerts'"
@folder-selected="updateActiveFolderId"
@@ -50,8 +50,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Custom File Input Section with Folder Dropdown -->
<template #file-input-section="{ jsonFiles, updateFiles }">
<div class="mb-1 flex w-[calc(100%-0.625rem)] items-start gap-2">
<div class="w-[69%]">
<div class="mb-1 flex w-[calc(100%-0.625rem)] items-start gap-2 max-md:flex-wrap">
<div class="w-[69%] max-md:w-full">
<OFile
data-test="alert-import-json-file-input"
:model-value="jsonFiles"
@@ -64,7 +64,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="md"
/>
</div>
<div class="w-[30%]">
<div class="w-[30%] max-md:w-full">
<SelectFolderDropDown
:type="'alerts'"
@folder-selected="updateActiveFolderId"
@@ -76,7 +76,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Output Section with Alert-specific Error Display -->
<template #output-content>
<div class="border-border-default flex h-full w-full min-w-100 flex-col border-s">
<div
class="border-border-default flex h-full w-full min-w-100 flex-col border-s max-md:min-w-0"
>
<div
v-if="alertErrorsToDisplay.length > 0"
class="text-text-heading shrink-0 py-3 text-center text-sm font-semibold"
@@ -27,7 +27,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<!-- Output Section with Destination-specific Error Display -->
<template #output-content>
<div class="border-border-default flex h-full w-full min-w-100 flex-col border-s">
<div
class="border-border-default flex h-full w-full min-w-100 flex-col border-s max-md:min-w-0"
>
<div
v-if="destinationErrorsToDisplay.length > 0 || destinationCreators.length > 0"
class="text-text-heading shrink-0 py-3 text-center text-sm font-semibold"
@@ -42,7 +44,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ t("alert_destinations.import.outputMessages") }}
</div>
<OSeparator class="mt-1 shrink-0" />
<div class="min-h-0 w-full min-w-100 flex-1 [resize:none] overflow-auto">
<div class="min-h-0 w-full min-w-100 flex-1 [resize:none] overflow-auto max-md:min-w-0">
<!-- Destination Errors Section -->
<div class="error-section mb-2.5 p-2.5" v-if="destinationErrorsToDisplay.length > 0">
<div class="error-list">
+4 -2
View File
@@ -27,7 +27,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<!-- Output Section with Template-specific Error Display -->
<template #output-content>
<div class="border-border-default flex h-full w-full min-w-100 flex-col border-s">
<div
class="border-border-default flex h-full w-full min-w-100 flex-col border-s max-md:min-w-0"
>
<div
v-if="templateErrorsToDisplay.length > 0 || tempalteCreators.length > 0"
class="text-text-heading shrink-0 py-3 text-center text-sm font-semibold"
@@ -42,7 +44,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ t("alert_templates.outputMessages") }}
</div>
<OSeparator class="mt-1 shrink-0" />
<div class="min-h-0 w-full min-w-100 flex-1 [resize:none] overflow-auto">
<div class="min-h-0 w-full min-w-100 flex-1 [resize:none] overflow-auto max-md:min-w-0">
<!-- Template Errors Section -->
<div class="error-section mb-2.5 p-2.5" v-if="templateErrorsToDisplay.length > 0">
<div class="error-list">
+51 -3
View File
@@ -52,8 +52,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Toolbar: status toggle (All / Active / Resolved) + search — same
shape as the Alerts page tabs. -->
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OToggleGroup
mobile-dropdown
:model-value="statusFilter"
@update:model-value="(v) => filterByStatus(v as string)"
data-test="incident-status-filter-group"
@@ -77,7 +78,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OToggleGroup>
<OSearchInput
v-model="searchQuery"
class="min-w-0 flex-1"
class="min-w-0 flex-1 max-md:min-w-40"
:placeholder="t('alerts.incidents.search')"
data-test="incident-search-input"
clearable
@@ -196,6 +197,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="row.status === 'open'"
variant="ghost-warning"
size="icon-sm"
class="max-md:hidden"
@click.stop="acknowledgeIncident(row)"
data-test="incident-ack-btn"
><OIcon name="visibility" size="sm" /><OTooltip
@@ -205,6 +207,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="row.status !== 'resolved'"
variant="ghost-primary"
size="icon-sm"
class="max-md:hidden"
@click.stop="resolveIncident(row)"
data-test="incident-resolve-btn"
><OIcon name="task-alt" size="sm" /><OTooltip
@@ -214,11 +217,52 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="row.status === 'resolved'"
variant="ghost-warning"
size="icon-sm"
class="max-md:hidden"
@click.stop="reopenIncident(row)"
data-test="incident-reopen-btn"
><OIcon name="restart-alt" size="sm" /><OTooltip
:content="t('alerts.incidents.reopen')"
/></OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="incident-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
v-if="row.status === 'open'"
icon-left="visibility"
class="md:hidden"
data-test="incident-ack-btn-menu"
@select="acknowledgeIncident(row)"
>
<span>{{ t("alerts.incidents.acknowledge") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="row.status !== 'resolved'"
icon-left="task-alt"
class="md:hidden"
data-test="incident-resolve-btn-menu"
@select="resolveIncident(row)"
>
<span>{{ t("alerts.incidents.resolve") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="row.status === 'resolved'"
icon-left="restart-alt"
class="md:hidden"
data-test="incident-reopen-btn-menu"
@select="reopenIncident(row)"
>
<span>{{ t("alerts.incidents.reopen") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
@@ -238,7 +282,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Bottom -->
<template #bottom>
<div class="flex h-12 w-full items-center justify-between">
<div class="flex w-25 items-center text-xs font-normal">
<div class="flex w-25 items-center text-xs font-normal max-md:hidden">
{{ visibleIncidents.length }}
{{
visibleIncidents.length === 1
@@ -276,6 +320,8 @@ import OStatStrip from "@/lib/data/StatStrip/OStatStrip.vue";
import type { StatItem } from "@/lib/data/StatStrip/OStatStrip.types";
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import type { OTableColumnDef } from "@/lib/core/Table/OTable.types";
import { toast } from "@/lib/feedback/Toast/useToast";
import { COL } from "@/lib/core/Table/OTable.types";
@@ -296,6 +342,8 @@ export default defineComponent({
OStatStrip,
OToggleGroup,
OToggleGroupItem,
ODropdown,
ODropdownItem,
},
setup() {
const { t } = useI18nTyped();
@@ -110,6 +110,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="edit"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('alert_destinations.edit')"
@click="editDestination(row)"
>
@@ -120,11 +121,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="delete"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('alert_destinations.delete')"
@click="conformDeleteDestination(row)"
>
<OIcon name="delete" size="sm" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="alert-destination-list-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`alert-destination-list-${row.name}-update-destination-menu`"
@select="editDestination(row)"
>
<span>{{ t("alert_destinations.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`alert-destination-list-${row.name}-delete-destination-menu`"
@select="conformDeleteDestination(row)"
>
<span>{{ t("alert_destinations.delete") }}</span>
</ODropdownItem>
</ODropdown>
</template>
<template v-if="selectedDestinations.length > 0" #bottom>
@@ -196,6 +228,8 @@ import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
import OTable from "@/lib/core/Table/OTable.vue";
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import type { OTableColumnDef } from "@/lib/core/Table/OTable.types";
import { toast } from "@/lib/feedback/Toast/useToast";
import { COL } from "@/lib/core/Table/OTable.types";
@@ -225,6 +259,8 @@ export default defineComponent({
OTag,
OSearchInput,
OTable,
ODropdown,
ODropdownItem,
},
setup() {
const store = useStore();
+76 -13
View File
@@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="flex h-full flex-col p-0">
<OPageLayout
overflow-first
bleed
v-if="!showImportTemplate && !showTemplateEditor"
:title="t('alerts.header')"
@@ -29,13 +30,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<template #actions>
<OButton
variant="outline"
size="sm-action"
@click="importTemplate"
data-test="template-import"
>{{ t(`dashboard.import`) }}</OButton
>
<OButton
data-test="template-list-add-btn"
variant="primary"
@@ -44,6 +38,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>{{ t(`alert_templates.add`) }}</OButton
>
</template>
<template #actions-overflow>
<OButton
variant="outline"
size="sm-action"
@click="importTemplate"
data-test="template-import"
>{{ t(`dashboard.import`) }}</OButton
>
</template>
<div class="bg-card-glass-bg min-h-0 flex-1 overflow-hidden">
<OTable
ref="oTableRef"
@@ -71,8 +75,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:current-page="onPageChange"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OToggleGroup
mobile-dropdown
:model-value="activeTab"
@update:model-value="
(v: any) => {
@@ -96,7 +101,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OToggleGroup>
<OSearchInput
v-model="filterQuery"
class="flex-1"
class="flex-1 max-lg:min-w-0 max-md:min-w-40"
:placeholder="t('template.search')"
data-test="template-list-search-input"
/>
@@ -168,7 +173,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #cell-actions="{ row }">
<OButton
:title="t('alert_templates.exportTemplate')"
class="ms-1"
class="ms-1 max-md:hidden"
variant="ghost"
size="icon-sm"
@click.stop="exportTemplate(row)"
@@ -179,7 +184,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
<OButton
:data-test="`alert-template-list-${row.name}-update-template`"
class="ms-1"
class="ms-1 max-md:hidden"
variant="ghost"
size="icon-sm"
:title="
@@ -193,7 +198,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
<OButton
:data-test="`alert-template-list-${row.name}-clone-template`"
class="ms-1"
class="ms-1 max-md:hidden"
variant="ghost"
size="icon-sm"
:title="t('alert_templates.clone')"
@@ -204,7 +209,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
<OButton
:data-test="`alert-template-list-${row.name}-delete-template`"
class="ms-1"
class="ms-1 max-md:hidden"
variant="ghost"
size="icon-sm"
:title="
@@ -219,6 +224,62 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<OIcon name="delete" size="sm" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="alert-template-list-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="download"
class="md:hidden"
data-test="destination-export-menu"
@select="exportTemplate(row)"
>
<span>{{ t("alert_templates.exportTemplate") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:disabled="row.isPrebuilt"
:data-test="`alert-template-list-${row.name}-update-template-menu`"
@select="editTemplate(row)"
>
<span>{{
row.isPrebuilt
? t("alert_templates.systemReadOnlyEdit")
: t("alert_templates.edit")
}}</span>
</ODropdownItem>
<ODropdownItem
icon-left="content-copy"
class="md:hidden"
:data-test="`alert-template-list-${row.name}-clone-template-menu`"
@select="cloneTemplate(row)"
>
<span>{{ t("alert_templates.clone") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:disabled="row.isPrebuilt"
:data-test="`alert-template-list-${row.name}-delete-template-menu`"
@select="conformDeleteDestination(row)"
>
<span>{{
row.isPrebuilt
? t("alert_templates.systemReadOnlyDelete")
: t("alert_templates.delete")
}}</span>
</ODropdownItem>
</ODropdown>
</template>
<template #cell-used_by="{ row }">
<DependencyUsageCell
@@ -292,6 +353,8 @@ import OTable from "@/lib/core/Table/OTable.vue";
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import type { OTableColumnDef } from "@/lib/core/Table/OTable.types";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
import AlertSectionTabs from "@/components/alerts/AlertSectionTabs.vue";
+8 -1
View File
@@ -15,7 +15,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<OToggleGroup v-if="show" :model-value="activeTab" @update:model-value="onSelect">
<OToggleGroup
v-if="show"
:model-value="activeTab"
:mobile-dropdown="mobileDropdown"
@update:model-value="onSelect"
>
<OToggleGroupItem
v-for="tab in visibleTabs"
:key="tab.value"
@@ -77,11 +82,13 @@ const props = withDefaults(
size?: ToggleGroupItemSize;
// Tooltip shown when hovering an unsaved-changes dot (optional).
dirtyTitle?: I18nText;
mobileDropdown?: boolean;
}>(),
{
show: true,
size: "sm",
dirtyTitle: raw(""),
mobileDropdown: false,
},
);
+6 -2
View File
@@ -67,8 +67,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="logs-search-splitter h-full min-h-0 w-full"
v-model="splitterModel"
:style="splitterStyle"
:limits="[30, 60]"
:horizontal="false"
:limits="[30, stackPanes ? 100 : 60]"
:horizontal="stackPanes"
>
<template #before>
<div class="border-border-default flex h-full w-full flex-col border-e">
@@ -218,6 +218,7 @@ import OFile from "@/lib/forms/File/OFile.vue";
import { toast } from "@/lib/feedback/Toast/useToast";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
import OSplitter from "@/lib/core/Splitter/OSplitter.vue";
import useBreakpoint from "@/composables/useBreakpoint";
export default defineComponent({
name: "BaseImport",
@@ -325,6 +326,8 @@ export default defineComponent({
emits: ["back", "cancel", "import", "update:jsonStr", "update:jsonArray", "update:activeTab"],
setup(props, { emit }) {
const { t } = useI18nTyped();
const { lgUp } = useBreakpoint();
const stackPanes = computed(() => !lgUp.value);
// State
const jsonStr = ref<any>("");
@@ -519,6 +522,7 @@ export default defineComponent({
return {
raw,
t,
stackPanes,
jsonStr,
jsonFiles,
url,
+17 -7
View File
@@ -30,12 +30,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<component
:is="as"
class="bg-card-glass-bg rounded-default border-border-default flex flex-col gap-1 border px-3.5 py-2.5 transition-shadow duration-200 ease-in-out hover:shadow-md"
class="bg-card-glass-bg rounded-default border-border-default flex flex-col gap-1 border px-3.5 py-2.5 transition-shadow duration-200 ease-in-out hover:shadow-md max-lg:shrink-0 max-lg:basis-auto max-lg:flex-row-reverse max-lg:items-center max-lg:gap-1.5 max-lg:px-1.5 max-lg:py-1"
:data-test="dataTest"
:title="label && !lgUp ? String(label) : undefined"
>
<div class="mb-1 flex items-center justify-between gap-2">
<div class="mb-1 flex items-center justify-between gap-2 max-lg:mb-0">
<div
class="min-w-0 truncate"
class="min-w-0 truncate max-lg:hidden"
:class="labelClass || 'text-2xs text-text-secondary leading-normal font-semibold'"
>
<slot name="label">{{ label }}</slot>
@@ -44,7 +45,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="icon || $slots.icon"
class="rounded-default inline-flex shrink-0 items-center justify-center"
:class="[
iconSize === 'md' ? 'h-10 w-10' : 'h-6 w-6',
iconSize === 'md' ? 'h-10 w-10 max-lg:h-6 max-lg:w-6 max-lg:*:size-4' : 'h-6 w-6',
iconClass || 'bg-surface-subtle text-text-secondary',
]"
>
@@ -54,15 +56,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</span>
</div>
<div v-if="$slots.value" class="flex items-baseline gap-1">
<div
v-if="$slots.value"
class="flex items-baseline gap-1 max-lg:[&>:first-child]:text-lg max-lg:[&>:first-child]:leading-none"
>
<slot name="value" />
</div>
<div v-if="$slots.trend" class="text-3xs flex items-center gap-1 font-medium">
<div v-if="$slots.trend" class="text-3xs flex items-center gap-1 font-medium max-lg:hidden">
<slot name="trend" />
</div>
<slot name="footer" />
<div v-if="$slots.footer" class="contents max-lg:hidden">
<slot name="footer" />
</div>
</component>
</template>
@@ -70,6 +77,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { raw, type I18nText } from "@/types/i18n";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import type { IconName } from "@/lib/core/Icon/OIcon.icons";
import useBreakpoint from "@/composables/useBreakpoint";
const { lgUp } = useBreakpoint();
withDefaults(
defineProps<{
+6 -1
View File
@@ -31,7 +31,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
inset, so it lines up with the header and any table below it.
-->
<template>
<div class="grid" :class="gap" :style="gridStyle" data-test="kpi-card-row">
<div
class="grid max-lg:flex max-lg:flex-wrap max-lg:gap-1.5"
:class="gap"
:style="gridStyle"
data-test="kpi-card-row"
>
<slot />
</div>
</template>
+169 -97
View File
@@ -17,109 +17,148 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- eslint-disable vue/v-on-event-hyphenation -->
<!-- eslint-disable vue/attribute-hyphenation -->
<template>
<div class="bg-surface-panel border-border-default flex h-full flex-col border-e pb-1">
<div class="folder-header bg-transparent">
<div
class="text-text-heading ps-page-edge flex items-center justify-between gap-2 py-1.5 pe-1.5 text-sm font-semibold"
<div
class="bg-surface-panel flex flex-col"
:class="mobileRail ? '' : 'border-border-default h-full border-e pb-1'"
>
<div
v-if="mobileRail"
class="flex items-center justify-between gap-1 py-1 ps-1.5 pe-1.5"
data-drawer-anchor="folder-list"
>
<OButton
variant="ghost"
size="sm"
data-test="folder-list-mobile-toggle"
@click="mobileDrawerOpen = true"
>
{{ t("dashboard.folders") }}
<div>
<OButton
variant="ghost"
size="icon"
@click.stop="addFolder"
data-test="dashboard-new-folder-btn"
:title="t('common.addFolder')"
>
<OIcon name="add" size="sm" />
</OButton>
</div>
</div>
<!-- Search Input -->
<div class="px-1.5 pb-1.5">
<OSearchInput
v-model="searchQuery"
data-test="folder-search"
:placeholder="t('dashboard.searchFolder')"
clearable
class="w-full"
/>
</div>
<template #icon-left><OIcon name="folder-outline" size="sm" /></template>
<span class="max-w-52 truncate">{{ activeFolderName || t("dashboard.folders") }}</span>
</OButton>
<OButton
variant="ghost"
size="icon"
@click.stop="addFolder"
data-test="dashboard-new-folder-btn"
:title="t('common.addFolder')"
>
<OIcon name="add" size="sm" />
</OButton>
</div>
<div class="folders-tabs flex-1 overflow-y-auto px-1.5">
<OTabs
orientation="vertical"
dense
v-model="activeFolderId"
data-test="dashboards-folder-tabs"
>
<OTab
v-for="tab in filteredTabs"
:key="tab.folderId"
:name="tab.folderId"
class="test-class min-h-6"
:data-test="`dashboard-folder-tab-${tab.folderId}`"
@click="onTabClick(tab.folderId)"
>
<component
:is="mobileRail ? 'ODrawer' : 'div'"
v-bind="railWrapperProps"
:class="mobileRail ? undefined : 'contents'"
>
<div class="flex min-h-0 flex-col" :class="mobileRail ? 'h-full' : 'flex-1'">
<div class="folder-header bg-transparent">
<div
class="folder-item group/row flex min-h-6 w-full flex-nowrap items-center gap-1.5"
:data-test="`dashboard-folder-tab-name-${tab.name}`"
v-if="!mobileRail"
class="text-text-heading ps-page-edge flex items-center justify-between gap-2 py-1.5 pe-1.5 text-sm font-semibold"
>
<!-- Fixed-width icon slot. Always rendered, so every folder name
starts on the same x-position whether or not it has an icon. -->
<FolderIcon
:token="iconFor(tab)"
:favorite="tab.folderId === FAVORITES_FOLDER_ID"
:data-test="`dashboard-folder-icon-${tab.name}`"
/>
<span
class="folder-name min-w-0 flex-1 truncate text-left"
:title="tab.name"
:data-test="`dashboard-folder-name-${tab.name}`"
>{{ tab.name }}</span
>
<!-- Kept in flow rather than overlaid: an overlay needs a backdrop
matching the row, and OTab only paints a hover background on
INACTIVE tabs, so it would read wrong on the open folder. -->
<div
v-if="tab.folderId.toLowerCase() != 'default' && tab.folderId !== FAVORITES_FOLDER_ID"
class="hidden shrink-0 items-center group-hover/row:flex has-[[data-state=open]]:flex"
>
<ODropdown side="bottom" align="start">
<template #trigger>
<OButton
size="icon"
variant="ghost"
icon-left="more-vert"
class="h-5 w-5"
data-test="dashboard-more-icon"
/>
</template>
<ODropdownItem
data-test="dashboard-edit-folder-icon"
@select="editFolder(tab.folderId)"
>
<template #icon-left>
<OIcon name="edit" size="xs" />
</template>
{{ t("common.edit") }}
</ODropdownItem>
<ODropdownItem
variant="destructive"
data-test="dashboard-delete-folder-icon"
@select="showDeleteFolderDialogFn(tab.folderId)"
>
<template #icon-left>
<OIcon name="delete" size="xs" />
</template>
{{ t("common.delete") }}
</ODropdownItem>
</ODropdown>
{{ t("dashboard.folders") }}
<div>
<OButton
variant="ghost"
size="icon"
@click.stop="addFolder"
data-test="dashboard-new-folder-btn"
:title="t('common.addFolder')"
>
<OIcon name="add" size="sm" />
</OButton>
</div>
</div>
</OTab>
</OTabs>
</div>
<!-- Search Input -->
<div class="px-1.5 pb-1.5" :class="mobileRail ? 'pt-1' : ''">
<OSearchInput
v-model="searchQuery"
data-test="folder-search"
:placeholder="t('dashboard.searchFolder')"
clearable
class="w-full"
/>
</div>
</div>
<div class="folders-tabs flex-1 overflow-y-auto px-1.5">
<OTabs
orientation="vertical"
dense
v-model="activeFolderId"
data-test="dashboards-folder-tabs"
>
<OTab
v-for="tab in filteredTabs"
:key="tab.folderId"
:name="tab.folderId"
class="test-class min-h-6"
:data-test="`dashboard-folder-tab-${tab.folderId}`"
@click="onTabClick(tab.folderId)"
>
<div
class="folder-item group/row flex min-h-6 w-full flex-nowrap items-center gap-1.5"
:data-test="`dashboard-folder-tab-name-${tab.name}`"
>
<!-- Fixed-width icon slot. Always rendered, so every folder name
starts on the same x-position whether or not it has an icon. -->
<FolderIcon
:token="iconFor(tab)"
:favorite="tab.folderId === FAVORITES_FOLDER_ID"
:data-test="`dashboard-folder-icon-${tab.name}`"
/>
<span
class="folder-name min-w-0 flex-1 truncate text-left"
:title="tab.name"
:data-test="`dashboard-folder-name-${tab.name}`"
>{{ tab.name }}</span
>
<!-- Kept in flow rather than overlaid: an overlay needs a backdrop
matching the row, and OTab only paints a hover background on
INACTIVE tabs, so it would read wrong on the open folder. -->
<div
v-if="
tab.folderId.toLowerCase() != 'default' && tab.folderId !== FAVORITES_FOLDER_ID
"
class="hidden shrink-0 items-center group-hover/row:flex has-[[data-state=open]]:flex max-md:flex"
@click="isMobile && $event.stopPropagation()"
>
<ODropdown side="bottom" align="start">
<template #trigger>
<OButton
size="icon"
variant="ghost"
icon-left="more-vert"
class="h-5 w-5"
data-test="dashboard-more-icon"
/>
</template>
<ODropdownItem
data-test="dashboard-edit-folder-icon"
@select="editFolder(tab.folderId)"
>
<template #icon-left>
<OIcon name="edit" size="xs" />
</template>
{{ t("common.edit") }}
</ODropdownItem>
<ODropdownItem
variant="destructive"
data-test="dashboard-delete-folder-icon"
@select="showDeleteFolderDialogFn(tab.folderId)"
>
<template #icon-left>
<OIcon name="delete" size="xs" />
</template>
{{ t("common.delete") }}
</ODropdownItem>
</ODropdown>
</div>
</div>
</OTab>
</OTabs>
</div>
</div>
</component>
</div>
<AddFolder
v-model:open="showAddFolderDialog"
@@ -143,10 +182,12 @@ import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
import OTab from "@/lib/navigation/Tabs/OTab.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
// @ts-nocheck
import { computed, defineComponent, onMounted, ref, watch } from "vue";
import useBreakpoint from "@/composables/useBreakpoint";
import { useStore } from "vuex";
import { useI18nTyped, raw } from "@/types/i18n";
@@ -172,6 +213,7 @@ export default defineComponent({
OTab,
OButton,
OSearchInput,
ODrawer,
ODropdown,
ODropdownItem,
},
@@ -186,6 +228,11 @@ export default defineComponent({
type: Boolean,
default: false,
},
// Off when the rail already lives inside another drawer (OPageLayout #sidebar), or the triggers double-wrap.
drawerOnMobile: {
type: Boolean,
default: true,
},
},
emits: ["update:folders", "update:activeFolderId"],
setup(props, { emit }) {
@@ -193,6 +240,25 @@ export default defineComponent({
const { t } = useI18nTyped();
const { showPositiveNotification, showErrorNotification } = useNotifications();
const activeFolderId = ref("");
const { isMobile } = useBreakpoint();
const mobileDrawerOpen = ref(false);
const mobileRail = computed(() => isMobile.value && props.drawerOnMobile);
const railWrapperProps = computed(() =>
mobileRail.value
? {
open: mobileDrawerOpen.value,
side: "left",
size: "sm",
bleed: true,
title: t("dashboard.folders"),
anchor: '[data-drawer-anchor="folder-list"]',
"onUpdate:open": (v: boolean) => (mobileDrawerOpen.value = v),
}
: {},
);
const activeFolderName = computed(
() => filteredTabs.value.find((f: any) => f.folderId === activeFolderId.value)?.name ?? "",
);
const showAddFolderDialog = ref(false);
const isFolderEditMode = ref(false);
const selectedFolderToEdit = ref(null);
@@ -295,6 +361,7 @@ export default defineComponent({
if (folderId === activeFolderId.value) {
emit("update:activeFolderId", folderId);
}
mobileDrawerOpen.value = false;
};
const filteredTabs = computed(() => {
@@ -313,6 +380,11 @@ export default defineComponent({
});
return {
isMobile,
mobileDrawerOpen,
mobileRail,
railWrapperProps,
activeFolderName,
t,
activeFolderId,
showAddFolderDialog,
@@ -104,7 +104,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="close"
:title="t('common.remove')"
:data-test="`dashboard-addpanel-config-delete-column-${idx}`"
class="absolute! top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100"
class="absolute! top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100 max-md:opacity-100"
@click.stop="removeColumn(idx)"
/>
</div>
@@ -22,6 +22,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="`dashboard-panel-container`"
:data-test-panel-id="props.data.id"
:data-test-panel-title="props.data.title"
:data-panel-type="props.data.type"
>
<div
:class="{
@@ -408,6 +409,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import useBreakpoint from "@/composables/useBreakpoint";
import {
defineComponent,
ref,
@@ -588,8 +590,10 @@ export default defineComponent({
// layout while hidden so the title's truncation point never moves when the
// pointer enters or leaves; `invisible` (visibility:hidden) also takes them
// out of the tab order and the a11y tree, so nothing hidden is reachable.
// Touch has no hover, so < md the controls are simply always shown.
const { isMobile } = useBreakpoint();
const hoverRevealClass = computed(() =>
isCurrentlyHoveredPanel.value ? "" : "invisible pointer-events-none",
isCurrentlyHoveredPanel.value || isMobile.value ? "" : "invisible pointer-events-none",
);
//for edit panel
@@ -16,11 +16,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="flex h-full min-h-0 w-full flex-1" data-test="panel-editor-container">
<div class="flex" :style="rowStyle">
<div class="flex max-md:flex-col" :style="rowStyle">
<!-- Chart Type Selection Sidebar -->
<div>
<div class="max-md:shrink-0">
<div
class="scroll bg-surface-panel! border-border-default flex h-full max-w-25 min-w-25 flex-col overflow-x-hidden overflow-y-auto border-e"
class="scroll bg-surface-panel! border-border-default flex h-full max-w-25 min-w-25 flex-col overflow-x-hidden overflow-y-auto border-e max-md:h-auto max-md:w-full max-md:max-w-full max-md:min-w-0 max-md:overflow-x-hidden max-md:border-e-0 max-md:border-b"
>
<ChartSelection
v-model:selectedChartType="dashboardPanelData.data.type"
@@ -38,7 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<!-- Collapsed field list bar -->
<div
v-if="!dashboardPanelData.layout.showFieldList"
v-if="!dashboardPanelData.layout.showFieldList && !isMobile"
class="bg-surface-panel! border-border-default flex h-full w-12.5 shrink-0 cursor-pointer flex-col items-center justify-start overflow-y-auto border-e"
data-test="panel-editor-field-list-sidebar-collapsed"
@click="collapseFieldList"
@@ -59,6 +59,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-model="dashboardPanelData.layout.splitter"
:limits="splitterLimits"
:style="splitterStyle"
class="max-md:relative"
:before-class="fieldListPaneClass"
:disable="!dashboardPanelData.layout.showFieldList"
separatorClass="field-list-separator"
:separatorStyle="{
@@ -72,7 +74,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #before>
<div :class="fieldListWrapperClass">
<div
v-if="dashboardPanelData.layout.showFieldList"
v-if="dashboardPanelData.layout.showFieldList && !isMobile"
class="bg-surface-panel! flex flex-col"
:style="fieldListContainerStyle"
>
@@ -92,6 +94,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@scroll.passive="onBuilderScroll"
>
<div class="flex h-full w-full flex-col" :style="layoutPanelContainerStyle">
<div
v-if="isMobile"
class="border-border-default flex items-center border-b px-2 py-1.5"
data-drawer-anchor="panel-editor-fields"
>
<OButton
variant="outline"
size="sm"
icon-left="add"
data-test="panel-editor-mobile-fields-btn"
@click="mobileFieldsOpen = true"
>
{{ t("panel.fields") }}
</OButton>
</div>
<!-- Mode selection + Add To Dashboard row. Skip when empty (e.g.
dashboard mode) so its `my-2` margin isn't dead space. -->
<div
@@ -306,7 +323,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Config Panel Sidebar -->
<div
class="col-auto"
:class="configPanelClass"
:style="pageType === 'logs' || pageType === 'build' ? { height: '100%' } : {}"
>
<PanelSidebar
@@ -397,7 +414,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<!-- Collapsed field list bar for custom chart -->
<div
v-if="!dashboardPanelData.layout.showFieldList"
v-if="!dashboardPanelData.layout.showFieldList && !isMobile"
class="bg-surface-panel! border-border-default flex h-full w-12.5 shrink-0 cursor-pointer flex-col items-center justify-start overflow-y-auto border-e"
data-test="panel-editor-field-list-sidebar-collapsed"
@click="collapseFieldList"
@@ -417,9 +434,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSplitter
v-model="dashboardPanelData.layout.splitter"
:limits="[0, 20]"
class="max-md:relative"
:before-class="fieldListPaneClass"
:disable="!dashboardPanelData.layout.showFieldList"
:style="{
width: dashboardPanelData.layout.showFieldList ? '100%' : 'calc(100% - 3.125rem)',
width:
dashboardPanelData.layout.showFieldList || isMobile
? '100%'
: 'calc(100% - 3.125rem)',
height: '100%',
}"
separatorClass="field-list-separator"
@@ -440,7 +462,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #before>
<div :class="fieldListWrapperClass">
<div
v-if="dashboardPanelData.layout.showFieldList"
v-if="dashboardPanelData.layout.showFieldList && !isMobile"
class="bg-surface-panel! flex flex-col"
:style="fieldListContainerStyle"
>
@@ -458,6 +480,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:style="{ height: contentHeight, overflow: 'hidden' }"
>
<div class="scroll flex h-full min-w-0 flex-1 flex-col">
<div
v-if="isMobile"
class="border-border-default flex items-center border-b px-2 py-1.5"
data-drawer-anchor="panel-editor-fields"
>
<OButton
variant="outline"
size="sm"
icon-left="add"
data-test="panel-editor-mobile-fields-btn"
@click="mobileFieldsOpen = true"
>
{{ t("panel.fields") }}
</OButton>
</div>
<!-- Editor/Preview splitter -->
<div class="h-125 shrink-0 overflow-hidden">
<OSplitter
@@ -589,7 +626,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSeparator vertical />
<!-- Config Panel Sidebar for custom chart -->
<div class="col-auto">
<div :class="configPanelClass">
<PanelSidebar
:title="t('dashboard.configLabel')"
v-model="dashboardPanelData.layout.isConfigPanelOpen"
@@ -603,6 +640,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<!-- Stays open across adds so several fields can be placed on the axes in one visit. -->
<ODrawer
v-if="isMobile"
v-model:open="mobileFieldsOpen"
side="left"
size="sm"
bleed
:title="t('panel.fields')"
anchor='[data-drawer-anchor="panel-editor-fields"]'
data-test="panel-editor-mobile-fields-drawer"
>
<div class="flex h-full min-h-0 flex-col">
<PanelFieldList :editMode="editMode" frameless @collapse="mobileFieldsOpen = false" />
</div>
</ODrawer>
<!-- Legends Dialog -->
<ShowLegendsPopup
v-model:open="showLegendsDialog"
@@ -638,6 +691,7 @@ import type {
} from "./types/panelEditor";
import { resolveConfig } from "./types/panelEditor";
import { usePanelEditor } from "./composables/usePanelEditor";
import useBreakpoint from "@/composables/useBreakpoint";
import useDashboardPanelData from "@/composables/dashboard/useDashboardPanel";
// ============================================================================
@@ -656,6 +710,7 @@ import OIcon from "@/lib/core/Icon/OIcon.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OSplitter from "@/lib/core/Splitter/OSplitter.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
import type OverrideConfigComponent from "@/components/dashboards/addPanel/OverrideConfig.vue";
@@ -792,7 +847,7 @@ const {
handleResultMetadataUpdate,
metaDataValue,
seriesDataUpdate,
collapseFieldList,
collapseFieldList: baseCollapseFieldList,
layoutSplitterUpdated,
updateVrlFunctionFieldList,
onDataZoom,
@@ -814,6 +869,47 @@ const {
validatePanel,
});
// The desktop 20% field-list splitter is unusable on a phone, so there it opens at ~half width.
const { isMobile } = useBreakpoint();
const mobileFieldsOpen = ref(false);
const MOBILE_FIELD_SPLITTER = 45;
const collapseFieldList = (): void => {
baseCollapseFieldList();
if (isMobile.value && dashboardPanelData.layout.showFieldList) {
dashboardPanelData.layout.splitter = MOBILE_FIELD_SPLITTER;
}
};
// Page init reopens the pane after mount, so on phones only the user's toggle (MOBILE_FIELD_SPLITTER) may open it.
watch(
[isMobile, () => dashboardPanelData.layout.showFieldList],
([mobile, show]) => {
if (mobile && show && dashboardPanelData.layout.splitter !== MOBILE_FIELD_SPLITTER) {
dashboardPanelData.layout.splitter = 0;
dashboardPanelData.layout.showFieldList = false;
}
},
{ immediate: true },
);
// The field list and config sidebar can't share a phone-width row, so opening either closes the other.
watch(
() => isMobile.value && dashboardPanelData.layout.isConfigPanelOpen,
(configOpenOnMobile) => {
if (configOpenOnMobile && dashboardPanelData.layout.showFieldList) {
dashboardPanelData.layout.splitter = 0;
dashboardPanelData.layout.showFieldList = false;
}
},
);
watch(
() => isMobile.value && dashboardPanelData.layout.showFieldList,
(fieldsOpenOnMobile) => {
if (fieldsOpenOnMobile && dashboardPanelData.layout.isConfigPanelOpen) {
dashboardPanelData.layout.isConfigPanelOpen = false;
}
},
);
// ============================================================================
// Custom Chart State
// ============================================================================
@@ -898,11 +994,18 @@ const chartAreaStyle = computed(() => {
// Main content area class - logs needs flat background without card styling
const mainContentAreaClass = computed(() => {
if (props.pageType === "logs") {
return "flex bg-card-glass-bg";
return "flex bg-card-glass-bg max-md:relative";
}
return "flex bg-card-glass-bg h-full overflow-y-hidden";
return "flex bg-card-glass-bg h-full overflow-y-hidden max-md:relative";
});
const configPanelClass = computed(() => [
"col-auto max-md:min-w-0",
isMobile.value && dashboardPanelData.layout.isConfigPanelOpen
? "absolute inset-y-0 right-0 left-0 z-30"
: "",
]);
// Row style - logs/build needs height: 100%, others need overflow-y: auto
const rowStyle = computed<CSSProperties>(() => {
if (props.pageType === "logs" || props.pageType === "build") {
@@ -938,6 +1041,9 @@ const mainContentContainerStyle = computed<CSSProperties>(() => {
// Splitter limits - logs/build uses [0, 100], others use [0, 20]
const splitterLimits = computed<[number, number]>(() => {
if (isMobile.value) {
return [0, 60];
}
if (props.pageType === "logs" || props.pageType === "build") {
return [0, 100];
}
@@ -947,7 +1053,8 @@ const splitterLimits = computed<[number, number]>(() => {
// Splitter style
const splitterStyle = computed(() => {
return {
width: dashboardPanelData.layout.showFieldList ? "100%" : "calc(100% - 3.125rem)",
width:
dashboardPanelData.layout.showFieldList || isMobile.value ? "100%" : "calc(100% - 3.125rem)",
height: "100%",
};
});
@@ -992,6 +1099,12 @@ const layoutPanelContainerStyle = computed(() => {
});
// Field list wrapper class - logs/build doesn't need padding-bottom
const fieldListPaneClass = computed(() =>
isMobile.value && dashboardPanelData.layout.showFieldList
? "max-md:absolute max-md:inset-y-0 max-md:left-0 max-md:z-30 max-md:w-full!"
: "",
);
const fieldListWrapperClass = computed(() => {
if (props.pageType === "logs" || props.pageType === "build") {
return "w-full h-full";
@@ -16,10 +16,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="h-full">
<div class="w-25 p-0">
<ul class="m-0 flex list-none flex-wrap p-0">
<div class="w-25 p-0 max-md:w-full">
<ul class="m-0 flex list-none flex-wrap p-0 max-md:flex-nowrap max-md:overflow-x-auto">
<li
class="w-12.5"
class="w-12.5 max-md:shrink-0"
v-for="(item, index) in ChartsArray"
:key="index"
:class="[
@@ -85,7 +85,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-geo-maps-query-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-geo-maps-query-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-latitude-item-${latitudeLabel}-menu`"
>
<div
@@ -97,7 +97,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.latitude.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<DynamicFunctionPopUp
@@ -206,7 +206,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-geo-maps-query-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-geo-maps-query-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-longitude-item-${longitudeLabel}-menu`"
>
<div
@@ -218,7 +218,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.longitude.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<DynamicFunctionPopUp
@@ -327,7 +327,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-geo-maps-query-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-geo-maps-query-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-weight-item-${weightLabel}-menu`"
>
<div
@@ -339,7 +339,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.weight.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<DynamicFunctionPopUp
@@ -85,7 +85,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-maps-query-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-maps-query-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-name-item-${nameLabel}-menu`"
>
<div
@@ -97,7 +97,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.name.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<div>
@@ -210,7 +210,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-maps-query-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-maps-query-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-value_for_maps-item-${valueLabel}-menu`"
>
<div
@@ -222,7 +222,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.value_for_maps.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<div>
@@ -25,7 +25,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"
>
<!-- x axis container -->
<div class="flex w-full flex-row ps-3" v-if="dashboardPanelData.data.type != 'metric'">
<div
class="flex w-full flex-row ps-3 max-md:flex-col"
v-if="dashboardPanelData.data.type != 'metric'"
>
<div class="flex-1">
<div class="flex flex-row">
<div class="layout-name flex items-center whitespace-nowrap" :class="labelWidthClass">
@@ -111,7 +114,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<div
:data-test="`dashboard-x-item-${itemX?.alias}-menu`"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0 max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:style="{
width:
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
@@ -174,10 +177,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.type == 'stacked'
"
>
<div class="flex h-full flex-row ps-3">
<div
class="max-md:border-border-default flex h-full flex-row ps-3 max-md:border-t max-md:ps-0"
>
<!-- Separator between X and Breakdown/Pivot -->
<OSeparator vertical class="me-4" />
<div class="layout-name flex min-w-0 items-center whitespace-nowrap">
<OSeparator vertical class="me-4 max-md:hidden" />
<!-- shrink-0, not min-w-0: the nowrap label would overflow onto the drop-zone hint if it shrank. -->
<div class="layout-name flex min-w-0 items-center whitespace-nowrap max-lg:shrink-0">
<span
class="rounded-default bg-badge-orange-ol-text me-1.5 h-2 w-2 shrink-0"
aria-hidden="true"
@@ -276,7 +282,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<div
:data-test="`dashboard-b-item-${itemB?.alias}-menu`"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0 max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:style="{
width:
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
@@ -411,7 +417,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<div
:data-test="`dashboard-y-item-${itemY?.alias}-menu`"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0 max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:style="{
width:
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
@@ -545,7 +551,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<div
:data-test="`dashboard-z-item-${itemZ?.alias}-menu`"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0"
class="field-function-menu-popup dashboard-query-builder-dropdown overflow-hidden p-0 max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:style="{
width:
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
@@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="col-auto" data-test="dashboard-panel-searchbar">
<div
class="sql-bar bg-section-header-bg border-border-default flex h-10 flex-row items-center justify-between gap-x-3 border-t border-b"
class="sql-bar bg-section-header-bg border-border-default flex h-10 flex-row items-center justify-between gap-x-3 border-t border-b max-md:h-auto max-md:flex-wrap max-md:gap-y-1 max-md:px-2 max-md:py-1"
@click.stop
>
<div
@@ -161,7 +161,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ multiQueryWarning }}
</OTag>
</div>
<div class="flex shrink-0 items-center gap-3">
<div class="flex shrink-0 items-center gap-3 max-md:max-w-full max-md:flex-wrap">
<OSwitch
data-test="logs-search-bar-show-query-toggle-btn"
v-model="dashboardPanelData.layout.vrlFunctionToggle"
@@ -179,7 +179,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<div
class="flex flex-1 flex-col overflow-hidden"
class="flex flex-1 flex-col overflow-hidden max-md:h-80 max-md:flex-none"
:style="!dashboardPanelData.layout.showQueryBar ? 'height: 0; flex: none;' : ''"
data-test="dashboard-query"
>
@@ -85,7 +85,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-sankey-chart-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-sankey-chart-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-source-item-${sourceLabel}-menu`"
>
<div
@@ -97,7 +97,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.source.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<div>
@@ -211,7 +211,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-sankey-chart-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-sankey-chart-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-target-item-${targetLabel}-menu`"
>
<div
@@ -223,7 +223,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.target.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<div>
@@ -336,7 +336,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</template>
<div
class="field-function-menu-popup dashboard-sankey-chart-builder-dropdown w-[48.1875rem]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md"
class="field-function-menu-popup dashboard-sankey-chart-builder-dropdown w-[min(48.1875rem,calc(100vw-1.5rem))]! translate-y-2 overflow-hidden rounded-none p-0 shadow-md max-lg:max-h-[var(--reka-popper-available-height,75vh)] max-lg:overflow-y-auto"
:data-test="`dashboard-value-item-${valueLabel}-menu`"
>
<div
@@ -348,7 +348,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex]
.fields.value.isDerived
? 'auto'
: '48.1875rem',
: 'min(48.1875rem, calc(100vw - 1.5rem))',
}"
>
<div>
@@ -1,8 +1,11 @@
<!-- Copyright 2026 OpenObserve Inc. -->
<template>
<div class="bg-surface-panel border-border-default flex h-full w-full flex-col border-e">
<div class="px-page-edge my-3 flex shrink-0 items-center justify-between">
<div
class="bg-surface-panel flex h-full w-full flex-col"
:class="frameless ? '' : 'border-border-default border-e'"
>
<div v-if="!frameless" class="px-page-edge my-3 flex shrink-0 items-center justify-between">
<span class="text-base font-bold">{{ t("panel.fields") }}</span>
<OButton
variant="outline"
@@ -421,6 +424,8 @@ import type { FieldItem } from "@/lib/lists/FieldList/OFieldList.types";
const props = defineProps<{
editMode?: boolean;
hideAllFieldsSelection?: boolean;
/** Drop the pane's own title row + border when a host (drawer) provides them. */
frameless?: boolean;
}>();
const dashboardPanelDataPageKey: string = inject("dashboardPanelDataPageKey", "dashboard");
@@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div
class="bg-surface-panel relative flex h-full min-h-0 flex-col"
:class="isOpen ? 'w-75' : 'w-12.5'"
:class="isOpen ? 'w-75 max-md:w-full' : 'w-12.5'"
data-test="panel-sidebar-root"
>
<div
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div>
<div class="flex gap-3">
<div class="flex gap-3 max-md:flex-wrap max-md:gap-1">
<!-- Query Type: SQL / PromQL -->
<OToggleGroup
v-if="showQueryType"
@@ -71,7 +71,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OButton
variant="ghost"
size="icon"
class="ms-1 shrink-0 opacity-0 transition-opacity duration-200 ease-in-out group-hover:opacity-100"
class="ms-1 shrink-0 opacity-0 transition-opacity duration-200 ease-in-out group-hover:opacity-100 max-md:opacity-100"
data-test="dashboard-legend-copy-btn"
:data-copied="isLegendCopied(Number(index)) ? 'true' : undefined"
@click.stop="copyLegend(legend.name, Number(index))"
@@ -3,11 +3,11 @@
<template>
<div data-test="dynamic-function-popup-root" class="flex flex-col">
<!-- Body -->
<div :class="fullMode ? 'flex min-h-0' : 'flex flex-col'">
<div :class="fullMode ? 'flex min-h-0 max-md:flex-col' : 'flex flex-col'">
<!-- Property pane -->
<div
class="flex w-52 shrink-0 flex-col gap-3.5 p-3"
:class="fullMode ? 'border-border-default border-e' : ''"
class="flex w-52 shrink-0 flex-col gap-3.5 p-3 max-md:w-full"
:class="fullMode ? 'border-border-default border-e max-md:border-e-0 max-md:border-b' : ''"
>
<div class="text-compact font-semibold" data-test="dynamic-function-popup-property-label">
{{ t("dashboard.dynamicFunctionPopUp.property") }}
@@ -82,6 +82,7 @@ import {
inject,
} from "vue";
import { useStore } from "vuex";
import useBreakpoint from "@/composables/useBreakpoint";
import { useTheme } from "@/composables/useTheme";
import { chartColor } from "@/utils/chartTheme";
import * as echarts from "echarts/core";
@@ -643,6 +644,35 @@ export default defineComponent({
zeroSizeReinitObserver.observe(chartRef.value);
};
// < lg the window resize listener misses container-only size changes (rails collapsing, stacked panes).
let containerResizeObserver: ResizeObserver | null = null;
let containerResizeRaf = 0;
const { lgUp } = useBreakpoint();
let lastObservedSize = { w: 0, h: 0 };
const resizeToContainer = () => {
containerResizeRaf = 0;
if (!chartRef.value) return;
const w = chartRef.value.clientWidth;
const h = chartRef.value.clientHeight;
if (w > 0 && h > 0 && (w !== lastObservedSize.w || h !== lastObservedSize.h)) {
lastObservedSize = { w, h };
chart?.resize();
}
};
const observeContainerResize = () => {
if (!chartRef.value || containerResizeObserver) return;
lastObservedSize = {
w: chartRef.value.clientWidth,
h: chartRef.value.clientHeight,
};
// Coalesced to one resize per frame: drawer and splitter animations notify every frame.
containerResizeObserver = new ResizeObserver(() => {
if (lgUp.value) return;
if (!containerResizeRaf) containerResizeRaf = requestAnimationFrame(resizeToContainer);
});
containerResizeObserver.observe(chartRef.value);
};
onMounted(async () => {
try {
await nextTick();
@@ -661,6 +691,7 @@ export default defineComponent({
if (chartRef.value.clientWidth === 0 || chartRef.value.clientHeight === 0) {
reinitWhenSized();
}
observeContainerResize();
}
chart?.setOption(withChartFont(props?.data?.options || {}), {
lazyUpdate: true,
@@ -682,6 +713,11 @@ export default defineComponent({
zeroSizeReinitObserver?.disconnect();
zeroSizeReinitObserver = null;
containerResizeObserver?.disconnect();
containerResizeObserver = null;
cancelAnimationFrame(containerResizeRaf);
containerResizeRaf = 0;
// Cancel throttled functions
throttledSetHoveredSeriesName.cancel();
throttledSetHoveredSeriesIndex.cancel();
@@ -711,6 +711,13 @@ export default defineComponent({
opacity: 0.4;
}
/* Touch has no th:hover — keep the sort affordance faintly visible. */
@media (max-width: 47.99rem) {
.table-wrapper :deep(.pivot-sort-icon) {
opacity: 0.4;
}
}
.table-wrapper :deep(.pivot-sort-active) {
opacity: 1 !important;
}
@@ -81,7 +81,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="mt-4 text-base font-bold">
{{ t("dashboard.addGeneralSettings") }}
</div>
<div class="mt-3 flex gap-4">
<div class="mt-3 flex gap-4 max-md:flex-col max-md:gap-2">
<div class="flex flex-1 flex-col">
<OFormInput
name="name"
@@ -110,7 +110,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
line at the bottom of that select. Bottom-aligning would shove the
error-free sibling down to stay flush; top-aligning keeps both
inputs aligned and lets the error hang below. -->
<div class="flex items-start gap-x-4">
<div class="flex items-start gap-x-4 max-md:flex-col max-md:gap-y-2">
<OFormSelect
name="query_data.stream_type"
:label="t('dashboard.selectStreamType')"
@@ -98,6 +98,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click.stop="editItem(row.tabId)"
data-test="dashboard-tab-settings-tab-edit-btn"
icon-left="edit"
class="max-md:hidden"
>
</OButton>
<OButton
@@ -107,9 +108,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('dashboard.delete')"
@click.stop="deleteItem(row.tabId)"
data-test="dashboard-tab-settings-tab-delete-btn"
class="max-md:hidden"
>
<template #icon-left><OIcon name="delete" size="sm" /></template>
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="dashboard-tab-settings-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
:disabled="row.tabId === editTabId"
class="md:hidden"
data-test="dashboard-tab-settings-tab-edit-btn-menu"
@select="editItem(row.tabId)"
>
<span>{{ t("dashboard.edit") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="currentDashboardData.data.tabs.length !== 1"
icon-left="delete"
variant="destructive"
class="md:hidden"
data-test="dashboard-tab-settings-tab-delete-btn-menu"
@select="deleteItem(row.tabId)"
>
<span>{{ t("dashboard.delete") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
</OTable>
@@ -145,6 +179,8 @@ import { deleteTab, editTab, getDashboard, updateDashboard } from "@/utils/commo
import { useRoute } from "vue-router";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTable from "@/lib/core/Table/OTable.vue";
import type { OTableColumnDef } from "@/lib/core/Table/OTable.types";
import { COL } from "@/lib/core/Table/OTable.types";
@@ -161,6 +197,8 @@ export default defineComponent({
AddTab,
TabsDeletePopUp,
OButton,
ODropdown,
ODropdownItem,
OIcon,
OTable,
},
@@ -171,6 +171,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click="editVariableFn(row.name)"
:data-test="`dashboard-edit-variable-${row.name}`"
icon-left="edit"
class="max-md:hidden"
>
</OButton>
<OButton
@@ -179,9 +180,40 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('dashboard.delete')"
@click.stop="showDeleteDialogFn({ row: { name: row.name } })"
data-test="dashboard-delete-variable"
class="max-md:hidden"
>
<template #icon-left><OIcon name="delete" size="sm" /></template>
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="dashboard-variable-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`dashboard-edit-variable-${row.name}-menu`"
@select="editVariableFn(row.name)"
>
<span>{{ t("dashboard.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
data-test="dashboard-delete-variable-menu"
@select="showDeleteDialogFn({ row: { name: row.name } })"
>
<span>{{ t("dashboard.delete") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
</OTable>
@@ -238,6 +270,8 @@ import VariablesDependenciesGraph from "./VariablesDependenciesGraph.vue";
import useNotifications from "@/composables/useNotifications";
import Sortable from "sortablejs";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import OTable from "@/lib/core/Table/OTable.vue";
@@ -253,6 +287,8 @@ export default defineComponent({
DashboardHeader,
VariablesDependenciesGraph,
OButton,
ODropdown,
ODropdownItem,
OIcon,
ODialog,
OTag,
@@ -102,7 +102,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
name="edit"
size="sm"
:aria-label="t('common.edit')"
class="text-text-secondary absolute top-1/2 right-0.5 -translate-y-1/2 cursor-pointer opacity-0 transition-opacity duration-150 group-hover/otab:opacity-60 hover:!opacity-100"
class="text-text-secondary absolute top-1/2 right-0.5 -translate-y-1/2 cursor-pointer opacity-0 transition-opacity duration-150 group-hover/otab:opacity-60 hover:!opacity-100 max-md:opacity-60"
:data-test="`dashboard-tab-${tab.tabId}-rename-btn`"
@click.stop="startRename(tab)"
@mousedown.stop
@@ -16,13 +16,19 @@
<!-- eslint-disable vue/no-unused-components -->
<template>
<div class="flex h-full flex-col overflow-hidden" data-test="view-panel-screen">
<div class="flex items-center justify-between p-3">
<div class="me-3 flex min-w-0 items-center text-xl tracking-[0.005em]">
<span class="truncate" data-test="dashboard-viewpanel-title">
<div class="flex items-center justify-between p-3 max-md:flex-wrap max-md:gap-y-2">
<div
class="me-3 flex min-w-0 items-center text-xl tracking-[0.005em] max-md:me-0 max-md:flex-1 max-md:basis-full"
>
<span
class="truncate"
:title="isMobile ? dashboardPanelData.data.title : undefined"
data-test="dashboard-viewpanel-title"
>
{{ dashboardPanelData.data.title }}
</span>
</div>
<div class="flex shrink-0 items-center gap-2">
<div class="flex shrink-0 items-center gap-2 max-md:ms-auto">
<!-- histogram interval for sql queries -->
<HistogramIntervalDropDown
v-if="!promqlMode && histogramFields.length"
@@ -178,6 +184,7 @@ import {
} from "vue";
import { useI18nTyped } from "@/types/i18n";
import useBreakpoint from "@/composables/useBreakpoint";
import { getDashboard, getPanel, checkIfVariablesAreLoaded } from "../../../utils/commons";
import { useRoute } from "vue-router";
import { useStore } from "vuex";
@@ -258,6 +265,7 @@ export default defineComponent({
const showLegendsDialog = ref(false);
const panelSchemaRendererRef: any = ref(null);
const { t } = useI18nTyped();
const { isMobile } = useBreakpoint();
const route = useRoute();
const store = useStore();
@@ -789,6 +797,7 @@ export default defineComponent({
});
return {
isMobile,
t,
setTimeForVariables,
dateTimeForVariables,
+1 -1
View File
@@ -73,7 +73,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- The affordance appears on hover rather than standing permanently, so
a strip of five does not read as five buttons. -->
<span
class="text-text-link text-2xs font-semibold opacity-0 transition-opacity group-hover:opacity-100"
class="text-text-link text-2xs font-semibold opacity-0 transition-opacity group-hover:opacity-100 max-md:opacity-100"
:class="activeId === insight.id ? 'opacity-100' : ''"
>
{{ affordanceOf(insight) }}
+1 -1
View File
@@ -55,7 +55,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<span
v-if="hasRun && mode !== 'button'"
class="text-text-secondary text-xs whitespace-nowrap tabular-nums select-none"
class="text-text-secondary text-xs whitespace-nowrap tabular-nums select-none max-md:hidden"
:data-test="`${dataTest}-age`"
>
{{ relative }}
+30 -1
View File
@@ -35,18 +35,45 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-xs-sq"
:icon-left="action.icon"
class="max-md:hidden"
:data-test="`${dataTest}-${action.id}`"
@click.stop="emit('action', action.id)"
>
<!-- Icon-only actions carry no label, so the tooltip names them; `left` opens it into the row, not over the row above. -->
<OTooltip side="left" :content="action.label" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
:data-test="moreTest"
@click.stop
/>
</template>
<ODropdownItem
v-for="action in actions"
:key="action.id"
:icon-left="action.icon"
class="md:hidden"
:data-test="`${dataTest}-${action.id}-menu`"
@select="emit('action', action.id)"
>
<span>{{ action.label }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import type { IconName } from "@/lib/core/Icon/OIcon.icons";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import type { I18nText } from "@/types/i18n";
@@ -57,7 +84,7 @@ export interface DbmRowAction {
label: I18nText;
}
withDefaults(
const props = withDefaults(
defineProps<{
actions?: DbmRowAction[];
dataTest?: string;
@@ -65,5 +92,7 @@ withDefaults(
{ actions: () => [], dataTest: "dbm-row-actions" },
);
const moreTest = computed(() => `${props.dataTest.replace(/-row-actions$/, "")}-row-more-actions`);
const emit = defineEmits<{ (e: "action", id: string): void }>();
</script>
+2 -2
View File
@@ -104,7 +104,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<button
type="button"
:aria-label="t('dbm.filters.removeScope')"
class="border-border-default bg-surface-base text-text-secondary hover:bg-surface-subtle-hover hover:text-text-heading focus-visible:ring-accent ring-offset-surface-base absolute -top-1 -right-1 z-10 inline-flex size-4 cursor-pointer items-center justify-center rounded-full border opacity-0 shadow-sm transition outline-none group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-1"
class="border-border-default bg-surface-base text-text-secondary hover:bg-surface-subtle-hover hover:text-text-heading focus-visible:ring-accent ring-offset-surface-base absolute -top-1 -right-1 z-10 inline-flex size-4 cursor-pointer items-center justify-center rounded-full border opacity-0 shadow-sm transition outline-none group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-1 max-md:opacity-100"
:data-test="`dbm-queries-scope-chip-${filter.key}-remove`"
@click="filter.onChange(null)"
>
@@ -135,7 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<button
type="button"
:aria-label="t('dbm.filters.removeScope')"
class="border-border-default bg-surface-base text-text-secondary hover:bg-surface-subtle-hover hover:text-text-heading focus-visible:ring-accent ring-offset-surface-base absolute -top-1 -right-1 z-10 inline-flex size-4 cursor-pointer items-center justify-center rounded-full border opacity-0 shadow-sm transition outline-none group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-1"
class="border-border-default bg-surface-base text-text-secondary hover:bg-surface-subtle-hover hover:text-text-heading focus-visible:ring-accent ring-offset-surface-base absolute -top-1 -right-1 z-10 inline-flex size-4 cursor-pointer items-center justify-center rounded-full border opacity-0 shadow-sm transition outline-none group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-1 max-md:opacity-100"
data-test="dbm-queries-scope-chip-insight-remove"
@click="emit('clearInsight')"
>
@@ -39,6 +39,8 @@ describe("DbmTableToolbar", () => {
* The row's shape is what eight tables shared: a flex row that may shrink to
* nothing, with a FIXED-width search box that may not. Lose `w-64` and the
* search box grows with the page, pushing the scope chips off the row.
* Below lg the row wraps and the search drops to its own line, because
* `overflow-hidden` cut the trailing controls off instead of moving them.
*/
it("keeps the search box at a fixed width inside a shrinkable row", () => {
const wrapper = mountToolbar();
@@ -54,7 +56,7 @@ describe("DbmTableToolbar", () => {
]),
);
expect(wrapper.get('[data-test="dbm-samples-search"]').element.parentElement?.className).toBe(
"w-64 shrink-0",
"w-64 shrink-0 max-lg:order-last max-lg:w-full max-lg:basis-full",
);
});
+4 -2
View File
@@ -25,8 +25,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
of the row without owning its contents.
-->
<template>
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<div class="w-64 shrink-0">
<div
class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden max-lg:flex-wrap max-lg:overflow-visible"
>
<div class="w-64 shrink-0 max-lg:order-last max-lg:w-full max-lg:basis-full">
<OSearchInput
:model-value="search"
:placeholder="placeholder"
+11 -2
View File
@@ -55,8 +55,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-model="splitterModel"
:limits="hideTestPanel ? [100, 100] : [30, 100]"
class="w-full overflow-hidden"
:horizontal="false"
:separator-class="hideTestPanel ? 'hidden' : 'w-[0.0625rem] bg-card-glass-border'"
:horizontal="isMobile"
:separator-class="
hideTestPanel
? 'hidden'
: isMobile
? 'h-[0.0625rem] bg-card-glass-border'
: 'w-[0.0625rem] bg-card-glass-border'
"
>
<template v-slot:before>
<!-- Workflows (hideTestPanel): drop the horizontal padding so the editor
@@ -220,6 +226,7 @@ import { useReo } from "@/services/reodotdev_analytics";
import { toast } from "@/lib/feedback/Toast/useToast";
import { useVrlPlaceholder, useJsPlaceholder } from "@/composables/useVrlPlaceholder";
import OSplitter from "@/lib/core/Splitter/OSplitter.vue";
import useBreakpoint from "@/composables/useBreakpoint";
import OForm from "@/lib/forms/Form/OForm.vue";
import { useOForm } from "@/lib/forms/Form/useOForm";
import { makeAddFunctionSchema, type AddFunctionForm } from "./AddFunction.schema";
@@ -300,6 +307,7 @@ export default defineComponent({
setup(props, { emit, expose }) {
const store: any = useStore();
const router = useRouter();
const { isMobile } = useBreakpoint();
const { track } = useReo();
// let beingUpdated: boolean = false;
@@ -665,6 +673,7 @@ export default defineComponent({
handleFunctionError,
vrlFunctionError,
splitterModel,
isMobile,
closeAddFunction,
confirmDialogMeta,
transformTypeOptions,
@@ -71,8 +71,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<!-- Toolbar: type filter + search -->
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OToggleGroup
mobile-dropdown
:model-value="selectedFilter"
@update:model-value="
(v) => {
@@ -98,7 +99,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSearchInput
data-test="enrichment-tables-search-input"
v-model="filterQuery"
class="ms-auto w-64"
class="ms-auto w-64 max-md:ms-0 max-md:w-auto max-md:min-w-40 max-md:flex-1"
:placeholder="t('function.searchEnrichmentTable')"
/>
</div>
@@ -235,6 +236,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('logStream.explore')"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
@click="exploreEnrichmentTable(row)"
icon-left="search"
data-row-action="view"
@@ -249,6 +251,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('logStream.schemaHeader')"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
@click="listSchema(row)"
icon-left="format-list-bulleted"
data-row-action="view"
@@ -266,6 +269,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('function.enrichmentTables')"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
@click="showAddUpdateFn(row)"
icon-left="edit"
data-row-action="edit"
@@ -277,10 +281,72 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('function.delete')"
variant="ghost-destructive"
size="icon-sm"
class="max-md:hidden"
@click="showDeleteDialogFn(row)"
icon-left="delete"
data-row-action="delete"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="enrichment-table-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
v-if="
!row.urlJobs ||
row.urlJobs.length === 0 ||
row.aggregateStatus === 'completed'
"
icon-left="search"
class="md:hidden"
:data-test="`${row.name}-explore-btn-menu`"
@select="exploreEnrichmentTable(row)"
>
<span>{{ t("logStream.explore") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="
!row.urlJobs ||
row.urlJobs.length === 0 ||
row.aggregateStatus === 'completed'
"
icon-left="format-list-bulleted"
class="md:hidden"
:data-test="`${row.name}-schema-btn-menu`"
@select="listSchema(row)"
>
<span>{{ t("logStream.schemaHeader") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="
!row.urlJobs ||
row.urlJobs.length === 0 ||
row.aggregateStatus === 'completed' ||
row.aggregateStatus === 'failed'
"
icon-left="edit"
class="md:hidden"
:data-test="`${row.name}-edit-btn-menu`"
@select="showAddUpdateFn(row)"
>
<span>{{ t("function.enrichmentTables") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`${row.name}-delete-btn-menu`"
@select="showDeleteDialogFn(row)"
>
<span>{{ t("function.delete") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
@@ -301,7 +367,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #bottom>
<div class="flex w-full items-center justify-between py-2">
<div class="me-4 flex items-center text-xs font-normal">
<div class="me-4 flex items-center text-xs font-normal max-md:hidden">
{{ resultTotal }} {{ t("function.enrichmentTables") }}
</div>
<OButton
@@ -438,6 +504,8 @@ import { useToast } from "@/lib/feedback/Toast/useToast";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
import PipelineSectionTabs from "@/components/pipeline/PipelineSectionTabs.vue";
@@ -463,6 +531,8 @@ export default defineComponent({
OToggleGroupItem,
OButton,
ODrawer,
ODropdown,
ODropdownItem,
OSearchInput,
OTooltip,
OIcon,
+46 -2
View File
@@ -63,7 +63,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="h-full w-full"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
data-test="functions-list-search-input"
v-model="filterQuery"
@@ -127,6 +127,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('function.updateTitle')"
data-test="function-list-edit-function-btn"
data-row-action="edit"
class="max-md:hidden"
@click="showAddUpdateFn({ row })"
icon-left="edit"
/>
@@ -136,6 +137,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('function.delete')"
data-test="function-list-delete-function-btn"
data-row-action="delete"
class="max-md:hidden"
@click="showDeleteDialogFn({ row })"
icon-left="delete"
/>
@@ -145,14 +147,52 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="account-tree"
:title="t('function.associatedPipelines')"
data-row-action="view"
class="max-md:hidden"
@click="getAssociatedPipelines({ row })"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="function-list-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
data-test="function-list-edit-function-btn-menu"
@select="showAddUpdateFn({ row })"
>
<span>{{ t("function.updateTitle") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
data-test="function-list-delete-function-btn-menu"
@select="showDeleteDialogFn({ row })"
>
<span>{{ t("function.delete") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="account-tree"
class="md:hidden"
data-test="function-list-associated-pipelines-menu"
@select="getAssociatedPipelines({ row })"
>
<span>{{ t("function.associatedPipelines") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
<template #bottom>
<div class="flex w-full items-center justify-between py-2">
<div class="me-4 flex items-center text-xs font-normal">
<div class="me-4 flex items-center text-xs font-normal max-md:hidden">
{{ resultTotal }} {{ t("function.header") }}
</div>
<OButton
@@ -245,6 +285,8 @@ import searchState from "@/composables/useLogs/searchState";
import OButton from "@/lib/core/Button/OButton.vue";
import OBadge from "@/lib/core/Badge/OBadge.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
import PipelineSectionTabs from "@/components/pipeline/PipelineSectionTabs.vue";
@@ -265,6 +307,8 @@ export default defineComponent({
OButton,
OBadge,
ODialog,
ODropdown,
ODropdownItem,
OSearchInput,
OTooltip,
},
@@ -36,13 +36,13 @@
/>
</template>
<template #tabs>
<div class="o2-input flex h-full items-center gap-4">
<div class="o2-input flex h-full items-center gap-4 max-md:h-auto max-md:basis-full">
<!-- Divider between the function name (header #title) and the language
toggle — they are separate controls sharing one header row. h-full +
the separator's own self-stretch span the full h-15 header row; my-2
insets it to ~2.75rem, so it runs past the subtitle line ("Add
Function") rather than stopping at the name. -->
<OSeparator vertical class="my-2" />
<OSeparator vertical class="my-2 max-md:hidden" />
<!-- Transform-type (language) selector -->
<div class="flex h-9 items-center gap-4">
<!-- A segmented toggle rather than a radio group: the two languages are
+1 -1
View File
@@ -73,7 +73,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:footer-title="t('iam.ingestionTokens')"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="filterQuery"
:placeholder="t('ingestion.searchToken')"
+1 -1
View File
@@ -78,7 +78,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:footer-title="t('synthetics.tokens.title')"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="filterQuery"
:placeholder="t('synthetics.tokens.searchPlaceholder')"
+37 -2
View File
@@ -51,7 +51,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:selected-ids="handleSelectedIdsUpdate"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="filterQuery"
:placeholder="t('iam.searchGroup')"
@@ -83,6 +83,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="edit"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('common.edit')"
@click="editGroup(row)"
>
@@ -93,11 +94,41 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="delete"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('common.delete')"
@click="showConfirmDialog(row)"
>
<OIcon name="delete" size="sm" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="iam-groups-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`iam-groups-edit-${row.group_name}-role-icon-menu`"
@select="editGroup(row)"
>
<span>{{ t("common.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`iam-groups-delete-${row.group_name}-role-icon-menu`"
@select="showConfirmDialog(row)"
>
<span>{{ t("common.delete") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
<template #empty>
@@ -109,7 +140,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</template>
<template #bottom>
<span class="text-xs font-normal">{{ rows.length }} {{ t("iam.groups") }}</span>
<span class="text-xs font-normal max-md:hidden"
>{{ rows.length }} {{ t("iam.groups") }}</span
>
<OButton
v-if="selectedGroups.length > 0"
data-test="iam-groups-bulk-delete-btn"
@@ -156,6 +189,8 @@ import { ref, onBeforeMount, computed } from "vue";
import AddGroup from "./AddGroup.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
import OTable from "@/lib/core/Table/OTable.vue";
import type { OTableColumnDef } from "@/lib/core/Table/OTable.types";
@@ -55,7 +55,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
table-id="iam-organizations-list"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="filterQuery"
:placeholder="t('organization.search')"
@@ -112,6 +112,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="organization-name-edit"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('iam.listOrganizations.edit')"
@click="renameOrganization(row)"
>
@@ -122,11 +123,43 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="organization-delete"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('iam.listOrganizations.deleteOrganization')"
@click="deleteOrganization(row)"
>
<OIcon name="delete" size="sm" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="organization-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
data-test="organization-name-edit-menu"
@select="renameOrganization(row)"
>
<span>{{ t("iam.listOrganizations.edit") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="canDeleteOrg(row)"
icon-left="delete"
variant="destructive"
class="md:hidden"
data-test="organization-delete-menu"
@select="deleteOrganization(row)"
>
<span>{{ t("iam.listOrganizations.deleteOrganization") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
</OTable>
@@ -154,6 +187,8 @@ import { useConfirmDialog } from "@/composables/useConfirmDialog";
import AddUpdateOrganization from "@/components/iam/organizations/AddUpdateOrganization.vue";
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import OCodeCell from "@/lib/core/Table/cells/OCodeCell.vue";
@@ -177,6 +212,8 @@ export default defineComponent({
AddUpdateOrganization,
OEmptyState,
OButton,
ODropdown,
ODropdownItem,
OTooltip,
OTag,
OPageLayout,
+15 -5
View File
@@ -28,8 +28,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div :style="{ marginTop: 0 }" class="app-table-container flex min-h-0 flex-1 flex-col">
<div class="bg-card-glass-bg mt-2.5 mb-2.5">
<div class="px-3 py-2">
<div class="mb-2 flex w-full items-center justify-between">
<div class="flex items-center">
<div
class="mb-2 flex w-full items-center justify-between max-md:flex-wrap max-md:gap-y-1.5"
>
<div class="flex items-center max-md:min-w-0 max-md:flex-wrap max-md:gap-y-1.5">
<OSelect
:loading="isOrgLoading"
:model-value="selectedOrganization?.value"
@@ -67,8 +69,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</div>
</div>
<div class="mb-2 flex w-full items-center justify-between">
<div v-if="selectedOrganization && activeType == 'table'" class="flex items-center">
<div
class="mb-2 flex w-full items-center justify-between max-md:flex-wrap max-md:gap-y-1.5"
>
<div
v-if="selectedOrganization && activeType == 'table'"
class="flex items-center max-md:min-w-0 max-md:flex-wrap max-md:gap-y-1.5"
>
<OSearchInput
data-test="pipeline-list-search-input"
v-model="searchQuery"
@@ -94,7 +101,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:model-value="handleApiCategorySelect"
/>
</div>
<div v-if="selectedOrganization" class="float-right ms-auto flex items-center">
<div
v-if="selectedOrganization"
class="float-right ms-auto flex items-center max-md:flex-wrap max-md:justify-end max-md:gap-y-1.5"
>
<div class="app-tabs-container me-3 h-9 w-fit">
<AppTabs
data-test="time-unit-tabs"
+35 -2
View File
@@ -8,6 +8,8 @@ import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import { useI18nTyped } from "@/types/i18n";
const { t } = useI18nTyped();
@@ -112,7 +114,7 @@ const isUnusedRole = (row: any): boolean => row?.user_count === 0;
@update:global-filter="emit('update:globalFilter', $event)"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
:model-value="globalFilter"
:placeholder="t('iam.searchRole')"
@@ -143,6 +145,7 @@ const isUnusedRole = (row: any): boolean => row?.user_count === 0;
data-row-action="edit"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('common.edit')"
@click="emit('edit', row)"
>
@@ -153,11 +156,41 @@ const isUnusedRole = (row: any): boolean => row?.user_count === 0;
data-row-action="delete"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('common.delete')"
@click="emit('delete', row)"
>
<OIcon name="delete" size="sm" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="iam-roles-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`iam-roles-edit-${row.role_name}-role-icon-menu`"
@select="emit('edit', row)"
>
<span>{{ t("common.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`iam-roles-delete-${row.role_name}-role-icon-menu`"
@select="emit('delete', row)"
>
<span>{{ t("common.delete") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
@@ -171,7 +204,7 @@ const isUnusedRole = (row: any): boolean => row?.user_count === 0;
</template>
<template #bottom>
<span class="text-xs font-normal">{{ data.length }} {{ t("iam.roles") }}</span>
<span class="text-xs font-normal max-md:hidden">{{ data.length }} {{ t("iam.roles") }}</span>
<OButton
v-if="(selectedIds?.length ?? 0) > 0"
data-test="iam-roles-bulk-delete-btn"
@@ -62,7 +62,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:selected-ids="handleSelectedIdsUpdate"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="filterQuery"
:placeholder="t('serviceAccounts.search')"
@@ -175,6 +175,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
icon-left="refresh"
class="max-md:hidden"
@click="confirmRefreshAction(row)"
/>
<OButton
@@ -184,6 +185,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
icon-left="edit"
class="max-md:hidden"
@click="addRoutePush(row)"
/>
<OButton
@@ -193,13 +195,52 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
icon-left="delete"
class="max-md:hidden"
@click="confirmDeleteAction(row)"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="service-accounts-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="refresh"
class="md:hidden"
data-test="service-accounts-refresh-menu"
@select="confirmRefreshAction(row)"
>
<span>{{ t("serviceAccounts.rotate") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="edit"
class="md:hidden"
data-test="service-accounts-edit-menu"
@select="addRoutePush(row)"
>
<span>{{ t("serviceAccounts.update") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
data-test="service-accounts-delete-menu"
@select="confirmDeleteAction(row)"
>
<span>{{ t("serviceAccounts.deleteServiceAccount") }}</span>
</ODropdownItem>
</ODropdown>
</template>
</template>
<template #bottom>
<span class="text-xs font-normal"
<span class="text-xs font-normal max-md:hidden"
>{{ serviceAccountsState.service_accounts_users.length }}
{{ t("serviceAccounts.header") }}</span
>
@@ -441,6 +482,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, onBeforeMount } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
@@ -489,6 +532,8 @@ export default defineComponent({
AddServiceAccount,
ConfirmDialog,
OButton,
ODropdown,
ODropdownItem,
ODialog,
OIcon,
OPageLayout,
@@ -42,7 +42,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
table-id="iam-invitations-list"
>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="filterQuery"
:placeholder="t('invitation.search')"
@@ -96,6 +96,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OButton
variant="primary"
size="sm"
class="max-md:hidden"
@click="acceptInvitation(row)"
:data-test="`accept-invitation-${row.token}`"
>
@@ -104,15 +105,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OButton
variant="secondary"
size="sm"
class="max-md:hidden"
@click="rejectInvitation(row)"
:data-test="`reject-invitation-${row.token}`"
>
{{ t("invitation.reject") }}
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="invitation-list-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
class="md:hidden"
:data-test="`accept-invitation-${row.token}-menu`"
@select="acceptInvitation(row)"
>
<span>{{ t("invitation.accept") }}</span>
</ODropdownItem>
<ODropdownItem
class="md:hidden"
:data-test="`reject-invitation-${row.token}-menu`"
@select="rejectInvitation(row)"
>
<span>{{ t("invitation.reject") }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
<template #bottom>
<span class="text-xs font-normal">
<span class="text-xs font-normal max-md:hidden">
{{ resultTotal }} {{ t("invitation.pendingInvitations") }}
</span>
</template>
@@ -151,6 +179,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import OTimeCell from "@/lib/core/Table/cells/OTimeCell.vue";
@@ -189,6 +219,8 @@ export default defineComponent({
OPageLayout,
OEmptyState,
OButton,
ODropdown,
ODropdownItem,
OTooltip,
OTag,
OTimeCell,
+58 -2
View File
@@ -85,7 +85,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OSearchInput
v-model="filterQuery"
:placeholder="t('user.search')"
@@ -167,6 +167,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('user.delete')"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:data-test="`delete-basic-user-${row.email}`"
data-row-action="delete"
@click="confirmDeleteAction(row)"
@@ -178,6 +179,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('user.revoke_invite')"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:data-test="`revoke-invite-${row.email}`"
data-row-action="delete"
@click="confirmRevokeAction(row)"
@@ -189,15 +191,65 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:title="t('user.update')"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:data-test="`edit-basic-user-${row.email}`"
data-row-action="edit"
@click="addRoutePush(row)"
>
<OIcon name="edit" size="sm" />
</OButton>
<ODropdown
v-if="
(row.enableDelete && row.status != 'pending') ||
(row.status == 'pending' && row.token) ||
(row.enableEdit && row.status != 'pending')
"
side="bottom"
align="end"
>
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="user-list-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
v-if="row.enableDelete && row.status != 'pending'"
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`delete-basic-user-${row.email}-menu`"
@select="confirmDeleteAction(row)"
>
<span>{{ t("user.delete") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="row.status == 'pending' && row.token"
icon-left="cancel"
variant="destructive"
class="md:hidden"
:data-test="`revoke-invite-${row.email}-menu`"
@select="confirmRevokeAction(row)"
>
<span>{{ t("user.revoke_invite") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="row.enableEdit && row.status != 'pending'"
icon-left="edit"
class="md:hidden"
:data-test="`edit-basic-user-${row.email}-menu`"
@select="addRoutePush(row)"
>
<span>{{ t("user.update") }}</span>
</ODropdownItem>
</ODropdown>
</template>
<template #bottom>
<span class="text-xs font-normal"
<span class="text-xs font-normal max-md:hidden"
>{{ rows.length }}
{{
isEnterpriseOrCloud
@@ -283,6 +335,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, onActivated, onBeforeMount, watch } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import OStatStrip from "@/lib/data/StatStrip/OStatStrip.vue";
@@ -325,6 +379,8 @@ export default defineComponent({
AddUser,
MemberInvitation,
OButton,
ODropdown,
ODropdownItem,
OTooltip,
OTag,
OStatStrip,
+113 -2
View File
@@ -15,7 +15,99 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<OSplitter v-model="categorySplitterModel" unit="px" class="h-full">
<div v-if="compact" class="flex h-full min-h-0 flex-col">
<div class="border-border-default shrink-0 border-b px-2">
<OTabs v-model="selectedCategory" dense>
<OTab
v-for="cat in aiCategories"
:key="cat.slug"
:name="cat.slug"
:label="raw(cat.name)"
:data-test="`ai-integrations-category-${cat.slug}`"
/>
</OTabs>
</div>
<div
class="border-border-default flex shrink-0 items-center border-b px-2 py-1"
data-drawer-anchor="ai-integrations-list"
>
<OButton
variant="ghost"
size="sm"
data-test="ai-integrations-list-mobile-btn"
@click="listDrawerOpen = true"
>
<template #icon-left><OIcon name="menu" size="sm" /></template>
{{ selectedIntegrationName || t("common.sidePanel") }}
</OButton>
</div>
<div
class="bg-card-glass-bg min-h-0 flex-1 overflow-auto pt-0.5"
data-test="ai-integrations-detail-pane"
>
<router-view />
</div>
<ODrawer
v-model:open="listDrawerOpen"
side="left"
size="sm"
bleed
seamless
anchor='[data-drawer-anchor="ai-integrations-list"]'
data-test="ai-integrations-list-mobile-drawer"
>
<div class="flex h-full flex-col">
<div class="ps-2 pe-4 pt-2">
<OSearchInput
data-test="ai-integrations-search-input"
v-model="integrationFilter"
clearable
class="indexlist-search-input w-full"
:placeholder="t('common.search')"
/>
</div>
<div class="min-h-0 flex-1 overflow-y-auto">
<OTabs
v-model="selectedIntegration"
orientation="vertical"
dense
class="px-1"
@update:model-value="onIntegrationPicked"
>
<OTab
v-for="integration in filteredIntegrations"
:key="integration.slug"
:name="integration.routeName"
:label="raw(integration.name)"
:data-test="`ai-integrations-item-${integration.slug}`"
>
<template #icon>
<img
v-if="
(integration.logo || integration.logoDark) && !failedLogos.has(integration.slug)
"
:src="(isDark && integration.logoDark) || integration.logo"
:alt="t('common.itemLogo', { name: integration.name })"
class="rounded-default h-4.5 w-4.5 flex-none object-contain"
loading="lazy"
referrerpolicy="no-referrer"
@error="onLogoError(integration.slug)"
/>
<span
v-else
class="rounded-default bg-theme-accent text-text-inverse text-3xs grid h-4.5 w-4.5 flex-none place-items-center leading-none font-bold"
aria-hidden="true"
>{{ integration.name.charAt(0) }}</span
>
</template>
</OTab>
</OTabs>
</div>
</div>
</ODrawer>
</div>
<OSplitter v-else v-model="categorySplitterModel" unit="px" class="h-full">
<template v-slot:before>
<div class="h-full w-full">
<div class="bg-surface-panel border-border-default h-full border-e">
@@ -117,11 +209,18 @@ import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
import OTab from "@/lib/navigation/Tabs/OTab.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OSplitter from "@/lib/core/Splitter/OSplitter.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import useBreakpoint from "@/composables/useBreakpoint";
export default defineComponent({
name: "AIIntegrationsPage",
components: { OTabs, OTab, OSearchInput, OSplitter },
components: { OTabs, OTab, OSearchInput, OSplitter, ODrawer, OButton, OIcon },
setup() {
const { lgUp } = useBreakpoint();
const compact = computed(() => !lgUp.value);
const listDrawerOpen = ref(false);
const { t } = useI18nTyped();
const store = useStore();
const { isDark } = useTheme();
@@ -214,7 +313,19 @@ export default defineComponent({
},
);
const selectedIntegrationName = computed(
() => filteredIntegrations.value.find((i) => i.routeName === selectedIntegration.value)?.name,
);
const onIntegrationPicked = (value: string | number) => {
navigateToIntegration(value);
listDrawerOpen.value = false;
};
return {
compact,
listDrawerOpen,
selectedIntegrationName,
onIntegrationPicked,
raw,
t,
store,
+1 -1
View File
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- eslint-disable vue/x-invalid-end-tag -->
<template>
<DataSourceSidebarLayout v-model="tabs" :splitter-width="250">
<DataSourceSidebarLayout v-model="tabs" :splitter-width="250" compact-mode="strip">
<template #tabs>
<ORouteTab
name="ingestLogs"
@@ -31,7 +31,109 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#tabs slot — it receives { tabs, filter } so you can render/filter yourself.
-->
<template>
<OSplitter v-model="splitterWidthModel" unit="px" :horizontal="false" class="h-full">
<div v-if="compact" class="flex h-full min-h-0 flex-col">
<div
v-if="compactMode === 'strip'"
class="border-border-default shrink-0 border-b px-2"
:data-test="panelDataTest || undefined"
>
<OTabs
:model-value="modelValue"
dense
:class="tabsClass"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<slot name="tabs" :tabs="filteredTabs" :filter="filter">
<ORouteTab
v-for="(tab, index) in filteredTabs"
:key="tab.name"
:title="tab.title || tab.name"
:default="index === 0"
:name="tab.name"
:to="tab.to"
:icon="tab.icon"
:label="tab.label"
:data-test="
tab.dataTest || (tabDataTestPrefix ? tabDataTestPrefix + tab.name : undefined)
"
/>
</slot>
</OTabs>
</div>
<div
v-else
class="border-border-default flex shrink-0 items-center border-b px-2 py-1"
data-drawer-anchor="data-source-sidebar"
>
<OButton
variant="ghost"
size="sm"
data-test="data-source-sidebar-mobile-btn"
@click="mobileSidebarOpen = true"
>
<template #icon-left><OIcon name="menu" size="sm" /></template>
{{ triggerLabel }}
</OButton>
</div>
<div class="min-h-0 flex-1 overflow-hidden">
<slot />
</div>
<ODrawer
v-if="compactMode !== 'strip'"
v-model:open="mobileSidebarOpen"
side="left"
size="sm"
bleed
seamless
anchor='[data-drawer-anchor="data-source-sidebar"]'
data-test="data-source-sidebar-mobile-drawer"
>
<div
:class="['h-full overflow-hidden', { 'pt-1.5': !searchable }]"
:data-test="panelDataTest || undefined"
>
<div v-if="searchable" class="p-2">
<OSearchInput
v-model="filter"
:data-test="searchDataTest || undefined"
clearable
class="indexlist-search-input w-full"
:placeholder="searchPlaceholder || t('common.search')"
/>
</div>
<OTabs
:model-value="modelValue"
orientation="vertical"
dense
:class="['px-1', tabsClass]"
@update:model-value="
(v) => {
emit('update:modelValue', v);
mobileSidebarOpen = false;
}
"
>
<slot name="tabs" :tabs="filteredTabs" :filter="filter">
<ORouteTab
v-for="(tab, index) in filteredTabs"
:key="tab.name"
:title="tab.title || tab.name"
:default="index === 0"
:name="tab.name"
:to="tab.to"
:icon="tab.icon"
:label="tab.label"
:data-test="
tab.dataTest || (tabDataTestPrefix ? tabDataTestPrefix + tab.name : undefined)
"
/>
</slot>
</OTabs>
</div>
</ODrawer>
</div>
<OSplitter v-else v-model="splitterWidthModel" unit="px" :horizontal="false" class="h-full">
<template #before>
<div class="h-full w-full">
<div class="bg-surface-panel border-border-default h-full border-e">
@@ -89,6 +191,10 @@ import ORouteTab from "@/lib/navigation/Tabs/ORouteTab.vue";
import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OSplitter from "@/lib/core/Splitter/OSplitter.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import useBreakpoint from "@/composables/useBreakpoint";
interface DataSourceTab {
name: string;
@@ -119,6 +225,8 @@ const props = withDefaults(
panelDataTest?: string;
/** Prefix to derive each tab's data-test as `${prefix}${tab.name}` (when tab.dataTest is unset). */
tabDataTestPrefix?: string;
/** < lg: "drawer" hides the rail behind a trigger; "strip" renders it as a horizontal tab row. */
compactMode?: "drawer" | "strip";
}>(),
{
modelValue: "",
@@ -130,6 +238,7 @@ const props = withDefaults(
tabsClass: "",
panelDataTest: "",
tabDataTestPrefix: "",
compactMode: "drawer",
},
);
@@ -149,6 +258,15 @@ watch(
);
const filter = ref("");
const { lgUp } = useBreakpoint();
const compact = computed(() => !lgUp.value);
const mobileSidebarOpen = ref(false);
// Nested layouts (signal rail + tool rail) each show a trigger; the active tab's label tells them apart.
const triggerLabel = computed(() => {
const active = props.tabs.find((tab) => tab.name === props.modelValue);
return active ? active.label : t("common.sidePanel");
});
const filteredTabs = computed(() => {
if (!props.searchable || !filter.value) {
return props.tabs;
@@ -56,11 +56,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Step: Services -->
<div class="mb-6">
<div
class="rounded-default flex cursor-pointer items-center justify-between px-3 py-2"
class="rounded-default flex cursor-pointer items-center justify-between px-3 py-2 max-md:flex-wrap max-md:gap-2"
:class="collapsibleHeaderClass"
@click="showServices = !showServices"
>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 max-md:min-w-0">
<OIcon :name="showServices ? 'expand-less' : 'expand-more'" size="sm" color="primary" />
<div class="text-sm font-semibold" :class="stepLabelClass">
{{ t("ingestion.selectServicesToMonitor") }}
@@ -70,7 +70,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ t("ingestion.selected") }}
</OTag>
</div>
<div class="flex gap-2" @click.stop>
<div class="flex gap-2 max-md:ms-auto max-md:shrink-0" @click.stop>
<OButton variant="ghost-primary" size="xs" @click="selectAll">{{
t("ingestion.selectAll")
}}</OButton>
@@ -136,11 +136,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="mb-6">
<div
class="rounded-default flex cursor-pointer items-center justify-between px-3 py-2"
class="rounded-default flex cursor-pointer items-center justify-between px-3 py-2 max-md:flex-wrap max-md:gap-2"
:class="collapsibleHeaderClass"
@click="showTargetRegions = !showTargetRegions"
>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 max-md:min-w-0">
<OIcon
:name="showTargetRegions ? 'expand-less' : 'expand-more'"
size="sm"
@@ -156,7 +156,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>{{ targetRegions.length }} {{ t("ingestion.selected") }}</OTag
>
</div>
<div class="flex gap-2" @click.stop>
<div class="flex gap-2 max-md:ms-auto max-md:shrink-0" @click.stop>
<OButton variant="ghost-primary" size="xs" @click="selectAllRegions">{{
t("ingestion.selectAll")
}}</OButton>
@@ -1116,6 +1116,16 @@ function fireConfetti() {
.step-inputs :deep(label) {
margin-bottom: 0.125rem;
}
@media (max-width: 47.9375rem) {
.step-inputs > *,
.step-inputs :deep(.w-field-width-md) {
max-width: 100%;
}
.variant-tabs {
max-width: 100%;
overflow-x: auto;
}
}
/* ---- steps (lib OStepper in expanded mode) — only the per-step body content
is styled here; the rail (indicator/connector/title) comes from OStepper. ---- */
+4 -1
View File
@@ -15,7 +15,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="bg-card-glass-bg h-[100vh] w-[100vw]">
<!-- dvh, not vh: mobile browser chrome overlaps a 100vh box. -->
<div
class="bg-card-glass-bg h-[100vh] w-[100vw] max-md:h-dvh max-md:w-full max-md:overflow-y-auto"
>
<div style="max-width: 25rem; padding-top: 6.25rem" class="mx-auto p-3">
<div
class="flex justify-center text-center"
+13 -6
View File
@@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #header-right>
<div
v-if="indexData.name"
class="rounded-default bg-surface-panel border-border-default flex items-center gap-1.5 border px-2 py-1"
class="rounded-default bg-surface-panel border-border-default flex items-center gap-1.5 border px-2 py-1 max-md:hidden"
>
<img
:src="getTimelineIcon"
@@ -70,7 +70,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
style="height: calc(100vh - 3.75rem)"
>
<!-- this the grid section the tiles section -->
<div class="stats-grid mb-2 grid grid-cols-4 gap-2">
<div class="stats-grid mb-2 grid grid-cols-4 gap-2 max-md:grid-cols-2">
<!-- Docs Count Tile -->
<div
v-if="store.state.zoConfig.show_stream_stats_doc_num"
@@ -170,7 +170,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="rounded-default bg-surface-base border-border-default flex h-full min-h-0 w-full flex-col overflow-hidden border p-2"
>
<div>
<div class="flex justify-start">
<!-- min-w-0/max-w-full let OTabs' own arrow-scroller engage instead of clipping silently. -->
<div class="flex justify-start max-md:max-w-full max-md:min-w-0">
<OTabs v-model="activeMainTab" dense>
<!-- Schema Settings Tab with conditional class -->
<OTab
@@ -224,7 +225,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<div v-if="indexData.defaultFts" class="mt-3 font-normal">
<label
class="bg-banner-warning-bg rounded-default border-banner-warning-border text-banner-warning-text border px-4 py-1 font-semibold"
class="bg-banner-warning-bg rounded-default border-banner-warning-border text-banner-warning-text border px-4 py-1 font-semibold max-md:inline-block max-md:max-w-full"
>
{{ t("logStream.mapping") }}
{{ t("logStream.defaultFtsKeysUsed") }}</label
@@ -338,10 +339,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- OTable fills the remaining height inside the schemaSettings flex column -->
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
<!-- horizontal-scroll: otherwise the elastic name column collapses to 0 beside the fixed ones. -->
<OTable
data-test="schema-log-stream-field-mapping-table"
:data="filteredSchemaData"
:columns="columns"
horizontal-scroll
row-key="name"
selection="multiple"
:selected-ids="selectedSchemaIds"
@@ -601,6 +604,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="schema-log-stream-field-mapping-table"
:data="redBtnRows"
:columns="redBtnColumns"
horizontal-scroll
row-key="index"
selection="multiple"
v-model:selected-ids="selectedDateIds"
@@ -646,8 +650,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div
class="bg-card-glass-solid sticky bottom-0 z-1 mt-auto w-full flex-shrink-0 px-2 py-1"
>
<div v-if="indexData.schema.length > 0" class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div
v-if="indexData.schema.length > 0"
class="flex items-center justify-between max-md:flex-wrap max-md:gap-y-1"
>
<div class="flex items-center gap-2 max-md:flex-wrap">
<span v-if="activeMainTab == 'schemaSettings'" class="px-2 py-2"
><strong> {{ selectedFields.length }}</strong>
{{ t("logStream.fieldsSelected") }}</span
@@ -29,7 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Output Section with Pipeline-specific Error Display -->
<template #output-content>
<div
class="border-border-default flex h-full w-full flex-col border-s"
class="border-border-default flex h-full w-full flex-col border-s max-md:min-w-0!"
style="min-width: 25rem"
>
<div
@@ -38,7 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="`pipeline-section-tab-${s.key}`"
>
<OIcon :name="s.icon" size="sm" class="shrink-0" />
<span>{{ s.label }}</span>
<span :class="s.key === activeSectionKey ? '' : 'max-md:hidden'">{{ s.label }}</span>
<span
v-if="s.count != null"
class="text-2xs rounded-full px-1.5 py-1 leading-none font-bold"
+29 -3
View File
@@ -73,8 +73,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div class="flex w-full items-center gap-2 max-lg:min-w-0 max-md:contents">
<OToggleGroup
mobile-dropdown
:model-value="activeTab"
@update:model-value="onTabChange"
data-test="pipeline-list-tabs"
@@ -92,7 +93,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ t("pipeline_list.tab_realtime") }}
</OToggleGroupItem>
</OToggleGroup>
<div class="min-w-0 flex-1">
<div class="min-w-0 flex-1 max-md:min-w-40">
<OInput
data-test="pipeline-list-search-input"
v-model="filterQuery"
@@ -161,6 +162,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:variant="row.enabled ? 'ghost-destructive' : 'ghost'"
size="icon-sm"
:icon-left="row.enabled ? 'pause' : 'play-arrow'"
class="max-md:hidden"
@click.stop="togglePipeline(row)"
>
<OTooltip
@@ -169,11 +171,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:shortcut-id="row.enabled ? 'pipelinesRowPause' : undefined"
/>
</OButton>
<!-- Hover-only preview with no click action, so it has no menu counterpart below md. -->
<OButton
:data-test="`pipeline-list-${row.name}-view-pipeline`"
variant="ghost"
size="icon-sm"
:title="t('pipeline.view')"
class="max-md:hidden"
icon-left="visibility"
>
<OTooltip max-width="none" side="left">
@@ -185,6 +189,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="edit"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
@click.stop="editPipeline(row)"
icon-left="edit"
>
@@ -222,6 +227,27 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="more-vert"
/>
</template>
<ODropdownItem
:data-test="`pipeline-list-${row.name}-pause-start-action-menu`"
class="md:hidden"
@select="togglePipeline(row)"
>
<template #icon-left>
<OIcon size="sm" :name="row.enabled ? 'pause' : 'play-arrow'" />
</template>
{{ row.enabled ? t("alerts.pause") : t("alerts.start") }}
</ODropdownItem>
<ODropdownItem
:data-test="`pipeline-list-${row.name}-update-pipeline-menu`"
class="md:hidden"
@select="editPipeline(row)"
>
<template #icon-left>
<OIcon size="sm" name="edit" />
</template>
{{ t("alerts.edit") }}
</ODropdownItem>
<ODropdownSeparator class="md:hidden" />
<ODropdownItem
:data-test="`pipeline-list-${row.name}-export-action`"
shortcut-id="pipelinesRowExport"
@@ -315,7 +341,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #bottom="bottomProps">
<div class="flex w-full items-center justify-between py-1">
<div class="me-4 flex items-center text-xs font-normal">
<div class="me-4 flex items-center text-xs font-normal max-md:hidden">
{{ bottomProps.totalRows }} {{ t("pipeline.header") }}
</div>
<div v-if="selectedPipelineIds.length > 0" class="flex items-center gap-2">
@@ -105,7 +105,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Bottom footer -->
<template #bottom="{ totalRows }">
<div class="me-4 flex items-center py-2 text-xs font-normal">
<div class="me-4 flex items-center py-2 text-xs font-normal max-md:hidden">
{{ t("pipeline.backfillJobLabel", { count: totalRows }, totalRows) }}
</div>
</template>
@@ -174,6 +174,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="canPauseJob(row)"
variant="ghost-destructive"
size="icon-sm"
class="max-md:hidden"
@click="confirmPauseJob(row)"
data-test="pause-job-btn"
icon-left="pause"
@@ -184,6 +185,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="canResumeJob(row)"
variant="ghost-success"
size="icon-sm"
class="max-md:hidden"
@click="confirmResumeJob(row)"
data-test="resume-job-btn"
icon-left="play-arrow"
@@ -194,6 +196,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="canEditJob(row.status)"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
@click="editJob(row)"
data-test="edit-job-btn"
icon-left="edit"
@@ -203,6 +206,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OButton
variant="ghost"
size="icon-sm"
class="max-md:hidden"
@click="viewJob(row)"
data-test="view-job-btn"
icon-left="visibility"
@@ -213,6 +217,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="canDeleteJob(row.status)"
variant="ghost-destructive"
size="icon-sm"
class="max-md:hidden"
@click="confirmDeleteJob(row)"
data-test="delete-job-btn"
icon-left="delete"
@@ -223,12 +228,79 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="row.error"
variant="ghost-destructive"
size="icon-sm"
class="max-md:hidden"
@click="showErrorDialog(row)"
data-test="error-indicator-btn"
icon-left="error"
>
<OTooltip :content="t('common.errorPrefix', { message: row.error })" />
</OButton>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="backfill-jobs-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
v-if="canPauseJob(row)"
icon-left="pause"
class="md:hidden"
data-test="pause-job-btn-menu"
@select="confirmPauseJob(row)"
>
<span>{{ t("pipeline.jobTooltipLabel") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="canResumeJob(row)"
icon-left="play-arrow"
class="md:hidden"
data-test="resume-job-btn-menu"
@select="confirmResumeJob(row)"
>
<span>{{ t("pipeline.resumeJob") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="canEditJob(row.status)"
icon-left="edit"
class="md:hidden"
data-test="edit-job-btn-menu"
@select="editJob(row)"
>
<span>{{ t("pipeline.editJob") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="visibility"
class="md:hidden"
data-test="view-job-btn-menu"
@select="viewJob(row)"
>
<span>{{ t("pipeline.viewDetails") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="canDeleteJob(row.status)"
icon-left="delete"
variant="destructive"
class="md:hidden"
data-test="delete-job-btn-menu"
@select="confirmDeleteJob(row)"
>
<span>{{ t("pipeline.deleteJobTooltip") }}</span>
</ODropdownItem>
<ODropdownItem
v-if="row.error"
icon-left="error"
class="md:hidden"
data-test="error-indicator-btn-menu"
@select="showErrorDialog(row)"
>
<span>{{ t("common.errorPrefix", { message: row.error }) }}</span>
</ODropdownItem>
</ODropdown>
</div>
</template>
</OTable>
@@ -313,6 +385,8 @@ import OSelect from "@/lib/forms/Select/OSelect.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTable from "@/lib/core/Table/OTable.vue";
import OTableColumnToggle from "@/lib/core/Table/sub-components/OTableColumnToggle.vue";
import useExternalColumnToggle from "@/composables/useExternalColumnToggle";
@@ -44,7 +44,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@update:model-value="onPipelineSelected"
:placeholder="t('pipeline.searchHistory')"
data-test="pipeline-history-search-select"
class="min-w-62.5"
class="min-w-62.5 max-md:min-w-0"
clearable
>
<template #empty>
@@ -219,7 +219,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<template #bottom="{ totalRows }">
<div class="me-4 flex items-center py-2 text-xs font-normal">
<div class="me-4 flex items-center py-2 text-xs font-normal max-md:hidden">
{{ totalRows }} {{ t("pipeline.header") }}
</div>
</template>
@@ -17,7 +17,7 @@
v-model="selectedMetric"
:options="metrics"
:label="t('metrics.metricSelector.metricName')"
class="showLabelOnTop max-w-125 min-w-75"
class="showLabelOnTop max-w-125 min-w-75 max-md:max-w-full max-md:min-w-0"
@update:model-value="onMetricSelect"
clearable
data-test="metric-selector"
@@ -23,7 +23,7 @@
<!-- Legend -->
<div
data-test="promql-builder-options-field-wrapper"
class="ms-2.5 flex flex-row items-center gap-2"
class="ms-2.5 flex flex-row items-center gap-2 max-md:ms-0 max-md:w-full"
>
<span
data-test="promql-builder-options-field-label"
@@ -32,7 +32,7 @@
>
<div
data-test="promql-builder-options-field-input-wrapper"
class="relative inline-block"
class="relative inline-block max-md:min-w-0 max-md:flex-1"
>
<OCombobox
v-model="
@@ -44,6 +44,7 @@
search-regex="(?:{([^}]*)(?:{.*})*$|([a-zA-Z-_]+)$)"
data-test="dashboard-promql-builder-legend"
:value-replace-fn="selectPromQlNameOption"
class="max-md:w-full!"
style="width: 16.25rem"
/>
<OIcon
@@ -68,7 +69,7 @@
<!-- Step Value -->
<div
data-test="promql-builder-options-field-wrapper"
class="ms-2.5 flex flex-row items-center gap-2"
class="ms-2.5 flex flex-row items-center gap-2 max-md:ms-0"
>
<span
data-test="promql-builder-options-field-label"
@@ -111,7 +112,7 @@
<!-- Query Type Select (Range/Instant) -->
<div
data-test="promql-builder-options-field-wrapper"
class="ms-2.5 flex flex-row items-center gap-2"
class="ms-2.5 flex flex-row items-center gap-2 max-md:ms-0"
>
<span
data-test="promql-builder-options-field-label"
@@ -26,9 +26,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #subnav>
<div
data-test="running-queries-filter-container"
class="px-page-edge flex items-center justify-start gap-3 py-2"
class="px-page-edge flex items-center justify-start gap-3 py-2 max-md:flex-wrap max-md:gap-2"
>
<OToggleGroup
mobile-dropdown
:model-value="selectedQueryTypeTab"
@update:model-value="onChangeQueryTab($event as 'summary' | 'all')"
data-test="running-queries-query-type-tabs"
@@ -48,7 +49,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:options="searchFieldOptions"
labelKey="label"
valueKey="value"
class="w-35 p-0"
class="w-35 p-0 max-md:w-28"
data-test="running-queries-search-fields-select"
@update:model-value="filterQuery = ''"
/>
@@ -56,7 +57,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSearchInput
v-if="selectedSearchField == 'all'"
v-model="filterQuery"
class="no-border o2-search-input"
class="no-border o2-search-input max-md:min-w-24 max-md:flex-1"
:placeholder="t('queries.search')"
data-test="running-queries-search-input"
/>
@@ -90,6 +90,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="format-list-bulleted"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('queries.queryList')"
data-test="queryList-btn"
@click="listSchema(row)"
@@ -98,10 +99,40 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="close"
variant="ghost-destructive"
size="icon-sm"
class="max-md:hidden"
:title="t('queries.cancelQuery')"
data-test="cancelQuery-btn"
@click="confirmDeleteAction(row)"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="running-queries-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="format-list-bulleted"
class="md:hidden"
data-test="queryList-btn-menu"
@select="listSchema(row)"
>
<span>{{ t("queries.queryList") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="close"
variant="destructive"
class="md:hidden"
data-test="cancelQuery-btn-menu"
@select="confirmDeleteAction(row)"
>
<span>{{ t("queries.cancelQuery") }}</span>
</ODropdownItem>
</ODropdown>
</template>
<template #cell-user_id="{ row }">
<OUserCell :value="row.user_id" />
@@ -142,6 +173,8 @@ import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import { useStore } from "vuex";
import QueryList from "@/components/queries/QueryList.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
@@ -159,6 +192,8 @@ export default defineComponent({
QueryList,
OEmptyState,
OButton,
ODropdown,
ODropdownItem,
OTooltip,
OToggleGroup,
OToggleGroupItem,
+17 -10
View File
@@ -30,9 +30,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="create-report-page flex min-h-0 w-full flex-1 flex-col"
>
<div class="bg-card-glass-bg flex min-h-0 flex-1 overflow-auto">
<div ref="addAlertFormRef" class="my-3 px-4" style="width: 64rem">
<!-- The [style*=width] variant caps the fields' inline widths: max-width beats an inline width. -->
<div
ref="addAlertFormRef"
class="my-3 w-256 px-4 max-md:max-w-full max-md:[&_[style*=width]]:max-w-full"
>
<OForm :id="formId" :form="form" class="create-report-form">
<div class="flex items-start gap-4 px-2 pt-3">
<div class="flex items-start gap-4 px-2 pt-3 max-md:flex-wrap">
<div data-test="add-report-name-input" class="o2-input">
<OFormInput
data-test="add-report-name-input"
@@ -107,10 +111,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- items-start (not items-center): keeps all selects
top-aligned when a validation error appears under one and
grows its cell taller. -->
<div class="flex items-start justify-start">
<div class="flex items-start justify-start max-md:flex-col max-md:gap-2">
<div
data-test="add-report-folder-select"
class="o2-input me-2 pt-0"
class="o2-input me-2 pt-0 max-md:w-full!"
style="width: 30%"
>
<OFormSelect
@@ -121,12 +125,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
required
:loading="isFetchingFolders"
@update:model-value="(v: any) => onFolderSelection(v, index)"
style="min-width: 15.625rem !important; width: 100% !important"
class="min-w-62.5! max-md:min-w-0!"
style="width: 100% !important"
/>
</div>
<div
data-test="add-report-dashboard-select"
class="o2-input me-2 pt-0"
class="o2-input me-2 pt-0 max-md:w-full!"
style="width: 30%"
>
<OFormSelect
@@ -137,12 +142,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
required
:loading="isFetchingDashboard || isFetchingFolders"
@update:model-value="(v: any) => onDashboardSelection(v, index)"
style="min-width: 15.625rem !important; width: 100% !important"
class="min-w-62.5! max-md:min-w-0!"
style="width: 100% !important"
/>
</div>
<div
data-test="add-report-tab-select"
class="o2-input pt-0"
class="o2-input pt-0 max-md:w-full!"
style="width: 30%"
>
<OFormSelect
@@ -152,7 +158,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:label="t('reports.dashboardTab')"
required
:loading="isFetchingDashboard || isFetchingFolders"
style="min-width: 15.625rem !important; width: 100% !important"
class="min-w-62.5! max-md:min-w-0!"
style="width: 100% !important"
/>
</div>
</div>
@@ -181,7 +188,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
{{ t("reports.reportFormat") }}
</div>
<div class="flex gap-3">
<div class="flex gap-3 max-md:flex-col">
<!-- Report Type -->
<div class="o2-input col-auto" data-test="add-report-type-select">
<OFormSelect
+78 -8
View File
@@ -35,16 +35,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<!-- Folder rail (fixed width) + table — matches the Alerts layout. -->
<div data-test="report-list-splitter" class="report-list-table flex min-h-0 flex-1">
<div
data-test="report-list-splitter"
class="report-list-table flex min-h-0 flex-1 max-md:flex-col"
>
<!-- Left: folder list -->
<div class="w-rail h-full shrink-0">
<div
class="w-rail max-md:border-border-default h-full shrink-0 max-md:h-auto max-md:w-full max-md:border-b"
>
<div class="h-full">
<FolderList type="reports" @update:activeFolderId="updateActiveFolderId" />
</div>
</div>
<!-- Right: report table -->
<div class="h-full min-w-0 flex-1">
<div class="h-full min-w-0 flex-1 max-md:h-auto max-md:min-h-0">
<div class="bg-card-glass-bg h-full">
<OTable
data-test="report-list-table"
@@ -67,7 +72,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<!-- Toolbar: Scheduled/Cached tabs + search (inline folder scope) + refresh -->
<template #toolbar>
<div class="flex w-full items-center gap-2">
<div
class="flex w-full items-center gap-2 max-lg:min-w-0 max-lg:flex-wrap max-lg:gap-y-1.5 max-md:contents"
>
<div class="app-tabs-container">
<AppTabs
class="tabs-selection-container"
@@ -81,7 +88,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"
/>
</div>
<div class="min-w-0 flex-1">
<!-- flex-1 is basis-0, so the min-w floor is what wraps the input before its scope chips spill. -->
<div
class="min-w-0 flex-1 max-md:order-last max-md:basis-full md:max-lg:min-w-80"
>
<OInput
v-model="dynamicQueryModel"
:placeholder="
@@ -108,7 +118,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="folder-outline"
data-test="report-list-search-scope-current"
:title="t('reports.searchThisFolderTitle')"
>{{ t("reports.searchThisFolder") }}</OToggleGroupItem
><span class="max-md:hidden">{{
t("reports.searchThisFolder")
}}</span></OToggleGroupItem
>
<OToggleGroupItem
value="all"
@@ -116,7 +128,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="search"
data-test="report-list-search-across-folders-toggle"
:title="t('reports.searchAllFoldersTitle')"
>{{ t("reports.searchAllFolders") }}</OToggleGroupItem
><span class="max-md:hidden">{{
t("reports.searchAllFolders")
}}</span></OToggleGroupItem
>
</OToggleGroup>
</template>
@@ -211,6 +225,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="icon-sm"
:icon-left="row.enabled ? 'pause' : 'play-arrow'"
:title="row.enabled ? t('alerts.pause') : t('alerts.start')"
class="max-md:hidden"
@click="toggleReportState(row)"
/>
@@ -222,6 +237,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
:title="t('alerts.edit')"
class="max-md:hidden"
@click="editReport(row)"
/>
@@ -232,6 +248,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost"
size="icon-sm"
:title="t('reports.moveToFolder')"
class="max-md:hidden"
@click="openMoveDialog(row)"
/>
@@ -243,8 +260,57 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="ghost-destructive"
size="icon-sm"
:title="t('alerts.delete')"
class="max-md:hidden"
@click="confirmDeleteReport(row)"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
:title="t('dashboard.moreActions')"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="report-list-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
v-if="!reportsStateLoadingMap[row.report_id]"
:icon-left="row.enabled ? 'pause' : 'play-arrow'"
class="md:hidden"
:data-test="`report-list-${row.name}-pause-start-report-menu`"
@select="toggleReportState(row)"
>
<span>{{ row.enabled ? t("alerts.pause") : t("alerts.start") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`report-list-${row.name}-edit-report-menu`"
@select="editReport(row)"
>
<span>{{ t("alerts.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="drive-file-move"
class="md:hidden"
:data-test="`report-list-${row.name}-move-report-menu`"
@select="openMoveDialog(row)"
>
<span>{{ t("reports.moveToFolder") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`report-list-${row.name}-delete-report-menu`"
@select="confirmDeleteReport(row)"
>
<span>{{ t("alerts.delete") }}</span>
</ODropdownItem>
</ODropdown>
</template>
<!-- Table footer: pagination + bulk actions -->
@@ -252,7 +318,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="flex h-12 w-full items-center justify-between">
<!-- Left: count + action buttons grouped together -->
<div class="flex items-center gap-2">
<div class="flex items-center text-xs font-normal whitespace-nowrap">
<div
class="flex items-center text-xs font-normal whitespace-nowrap max-md:hidden"
>
{{ resultTotal }} {{ t("reports.header") }}
</div>
<OButton
@@ -343,6 +411,8 @@ import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import { toast } from "@/lib/feedback/Toast/useToast";
import { COL } from "@/lib/core/Table/OTable.types";
import { useShortcuts } from "@/lib/vue-shortcut-manager";
@@ -16,18 +16,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<section
class="grid h-full grid-cols-2 gap-2"
class="grid h-full grid-cols-2 gap-2 max-md:grid-cols-4 max-md:gap-1.5"
data-test="rum-errors-kpi-cards"
:aria-label="t('rum.errorKpisAria')"
>
<article
v-for="card in cards"
:key="card.key"
class="bg-card-glass-bg rounded-default border-border-default flex min-w-0 flex-col items-start border px-2.5 py-1.5"
class="bg-card-glass-bg rounded-default border-border-default flex min-w-0 flex-col items-start border px-2.5 py-1.5 max-md:px-1.5"
:title="isMobile ? card.caption : undefined"
:data-test="`rum-errors-kpi-${card.key}-card`"
>
<span
class="text-text-label flex items-center gap-1.5 text-xs font-medium tracking-wide uppercase"
class="text-text-label max-md:text-3xs flex items-center gap-1.5 text-xs font-medium tracking-wide uppercase max-md:block max-md:w-full max-md:truncate max-md:tracking-normal max-md:normal-case"
>
{{ card.label }}
<OTag
@@ -35,6 +36,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:label="card.badge.label"
:variant="card.badge.variant"
size="xs"
class="max-md:hidden"
data-test="rum-errors-kpi-crash-free-badge"
/>
</span>
@@ -42,15 +44,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSkeleton v-if="loading" variant="title" class="w-16" />
<span
v-else
class="text-xl font-semibold tabular-nums"
class="text-xl font-semibold tabular-nums max-md:text-base"
:class="card.valueClass"
:data-test="`rum-errors-kpi-${card.key}-value`"
>{{ card.value }}</span
>
<small v-if="!loading" :data-test="`rum-errors-kpi-${card.key}-caption`">{{
card.caption
}}</small>
<small
v-if="!loading"
class="max-md:hidden"
:data-test="`rum-errors-kpi-${card.key}-caption`"
>{{ card.caption }}</small
>
</article>
</section>
</template>
@@ -61,6 +66,9 @@ import { useI18nTyped } from "@/types/i18n";
import OTag from "@/lib/core/Badge/OTag.vue";
import OSkeleton from "@/lib/feedback/Skeleton/OSkeleton.vue";
import { addCommasToNumber, formatLargeNumber } from "@/utils/formatters";
import useBreakpoint from "@/composables/useBreakpoint";
const { isMobile } = useBreakpoint();
export interface ErrorKpis {
totalErrors: number;
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<section
class="grid grid-cols-2 gap-2 p-2 lg:grid-cols-5"
class="grid grid-cols-5 gap-2 p-2 max-lg:gap-1.5 max-lg:p-1.5"
data-test="rum-sessions-metrics-strip"
:aria-label="t('rum.sessionMetricsAria')"
>
@@ -24,28 +24,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-for="card in cards"
:key="card.key"
type="button"
class="metric-card rounded-default bg-card-glass-bg enabled:hover:border-accent flex flex-col items-start gap-0.5 border p-3 text-left transition-colors enabled:cursor-pointer disabled:cursor-default"
class="metric-card rounded-default bg-card-glass-bg enabled:hover:border-accent flex flex-col items-start gap-0.5 border p-3 text-left transition-colors enabled:cursor-pointer disabled:cursor-default max-lg:min-w-0 max-lg:p-1.5"
:title="lgUp ? undefined : card.caption"
:class="card.key === activeCard ? 'border-accent' : 'border-border-default'"
:aria-pressed="card.key === activeCard"
:disabled="!card.selectable"
:data-test="`rum-sessions-metric-${card.key}-card`"
@click="card.selectable && emit('select', card.key)"
>
<span class="text-text-label text-xs font-medium tracking-wide uppercase">{{
card.label
}}</span>
<span
class="text-text-label max-lg:text-3xs text-xs font-medium tracking-wide uppercase max-lg:w-full max-lg:truncate max-lg:tracking-normal max-lg:normal-case"
>{{ card.label }}</span
>
<span class="flex items-baseline gap-1.5">
<span
class="text-2xl font-semibold tabular-nums"
class="text-2xl font-semibold tabular-nums max-lg:text-base"
:class="card.valueClass"
:data-test="`rum-sessions-metric-${card.key}-value`"
>{{ card.value }}</span
>
<span v-if="card.rate" class="text-text-secondary text-sm tabular-nums"
<span v-if="card.rate" class="text-text-secondary text-sm tabular-nums max-lg:hidden"
>· {{ card.rate }}</span
>
</span>
<small :class="card.captionClass || 'text-text-secondary'">{{ card.caption }}</small>
<small class="max-lg:hidden" :class="card.captionClass || 'text-text-secondary'">{{
card.caption
}}</small>
</button>
</section>
</template>
@@ -54,6 +58,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { durationFormatter } from "@/utils/zincutils";
import useBreakpoint from "@/composables/useBreakpoint";
const { lgUp } = useBreakpoint();
export type SessionsMetricCardKey = "sessions" | "errors" | "frustrated" | "duration" | "bounced";
+36 -1
View File
@@ -57,7 +57,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #toolbar>
<OSearchInput
v-model="filterQuery"
class="no-border o2-search-input w-64"
class="no-border o2-search-input w-64 max-md:w-full"
:placeholder="t('aiToolset.search')"
/>
</template>
@@ -96,6 +96,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="edit"
variant="ghost"
size="icon-sm"
class="max-md:hidden"
:title="t('common.edit')"
@click="editToolset(row)"
icon-left="edit"
@@ -105,10 +106,40 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="delete"
variant="ghost-destructive"
size="icon-sm"
class="max-md:hidden"
:title="t('common.delete')"
@click="confirmDeleteToolset(row)"
icon-left="delete"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="ai-toolsets-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`ai-toolset-${row.name}-edit-menu`"
@select="editToolset(row)"
>
<span>{{ t("common.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`ai-toolset-${row.name}-delete-menu`"
@select="confirmDeleteToolset(row)"
>
<span>{{ t("common.delete") }}</span>
</ODropdownItem>
</ODropdown>
</template>
</OTable>
</div>
@@ -136,6 +167,8 @@ import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { raw, useI18nTyped } from "@/types/i18n";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
@@ -158,6 +191,8 @@ export default defineComponent({
ConfirmDialog,
AddAiToolset,
OButton,
ODropdown,
ODropdownItem,
OTooltip,
OTag,
OSearchInput,
+35 -2
View File
@@ -90,7 +90,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="edit"
variant="ghost"
size="icon-sm"
class="ms-1"
class="ms-1 max-md:hidden"
:title="t('common.edit')"
@click="editCipherKey(row)"
icon-left="edit"
@@ -100,11 +100,40 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-row-action="delete"
variant="ghost-destructive"
size="icon-sm"
class="ms-1"
class="ms-1 max-md:hidden"
:title="t('common.delete')"
@click="confirmDeleteCipherKey(row)"
icon-left="delete"
/>
<ODropdown side="bottom" align="end">
<template #trigger>
<OButton
icon-left="more-vert"
variant="ghost"
size="icon-xs-sq"
class="md:hidden"
data-test="cipher-keys-row-more-actions"
@click.stop
/>
</template>
<ODropdownItem
icon-left="edit"
class="md:hidden"
:data-test="`cipherkey-list-${row.name}-update-menu`"
@select="editCipherKey(row)"
>
<span>{{ t("common.edit") }}</span>
</ODropdownItem>
<ODropdownItem
icon-left="delete"
variant="destructive"
class="md:hidden"
:data-test="`cipherkey-list-${row.name}-delete-menu`"
@select="confirmDeleteCipherKey(row)"
>
<span>{{ t("common.delete") }}</span>
</ODropdownItem>
</ODropdown>
</template>
<template v-if="selectedKeys.length > 0" #bottom>
<span class="text-text-body text-xs font-medium">
@@ -156,6 +185,8 @@ import AddCipherKey from "@/components/cipherkeys/AddCipherKey.vue";
import CipherKeysService from "@/services/cipher_keys";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OTable from "@/lib/core/Table/OTable.vue";
@@ -174,6 +205,8 @@ export default defineComponent({
AddCipherKey,
ConfirmDialog,
OButton,
ODropdown,
ODropdownItem,
OTooltip,
OSearchInput,
OTable,
@@ -311,7 +311,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Bottom -->
<template #bottom>
<div class="flex h-9 w-full items-center justify-between">
<div class="w-[15.625rem] text-xs font-normal">
<div class="w-[15.625rem] text-xs font-normal max-md:hidden">
{{
t("settings.correlation.serviceCountSingular", {
count: filteredGroupCount,

Some files were not shown because too many files have changed in this diff Show More