mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 00:08:55 +00:00
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:
co-authored by
github-actions[bot]
parent
82d76c8872
commit
be23422e5b
@@ -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",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. ---- */
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user