fix: i18n migration
This commit is contained in:
parent
c85af27745
commit
08b8087190
|
|
@ -3,13 +3,28 @@
|
|||
How the OpenObserve web app guarantees that user-facing text is translatable, and
|
||||
what to do when a check fires.
|
||||
|
||||
**Status: enforcement complete and green.** `lint:ci` 0 errors · `type-check:app`
|
||||
0 errors · spec-inclusive `type-check` 0 errors · `format:check` clean.
|
||||
**Status: enforcement complete.** `lint:ci` 0 errors · `type-check:app` **exit 0**
|
||||
(with `--composite false`, which the npm script passes, no TS6307 surfaces) ·
|
||||
`format:check` clean.
|
||||
|
||||
> **Known measurement gap:** the "spec-inclusive" `type-check` (tsconfig.vitest.json)
|
||||
> currently checks **no spec files at all** — its include is `src/**/*.spec.{ts,js}`,
|
||||
> and TypeScript globs do **not** brace-expand, so the pattern matches nothing.
|
||||
> Any past "spec-inclusive type-check 0 errors" claim was vacuous. Fixing it means
|
||||
> splitting the include into `src/**/*.spec.ts` + `src/**/*.spec.js` AND then fixing
|
||||
> the spec-only type errors that surface (e.g. `metricGrouping.spec.ts` assigns keys
|
||||
> that don't exist in en-US to `I18nKey` fixtures). Tracked in §9.
|
||||
|
||||
The mechanisms are done and so is the message cleanup — **0 fake plurals** remain in
|
||||
en-US. What is left is narrower: **non-English plural rules**, the last **6 `gt` sites**,
|
||||
and **one CI gap**. See [§9 Outstanding work](#9-outstanding-work) — that section is the
|
||||
to-do list, kept in sync with measurements.
|
||||
en-US, the 10 surviving `gt()` calls are each individually justified (§8), `TEXT_ATTRS`
|
||||
has been retired in favour of prop types (§8), and the dead-key sweep is complete (§9).
|
||||
What is left is narrower: **non-English plural rules** and **spec type-checking**
|
||||
(a broken tsconfig glob plus a missing PR gate, §9.3). See
|
||||
[§9 Outstanding work](#9-outstanding-work) — that section is the to-do list, kept in
|
||||
sync with measurements.
|
||||
|
||||
Converting the 71 dynamic key sites so they can be type-checked was **considered and
|
||||
declined** — it is recorded under §4 as an accepted limit, not pending work.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -89,11 +104,11 @@ only two remaining `(s)` values are the seconds unit. Keep it that way.
|
|||
`{{ t('common.save') }}` · `:label="t('x')"` · `:label="row.name"` (a variable
|
||||
contributes no literal) · `:label="'—'"` (no letters).
|
||||
|
||||
### Two curated lists
|
||||
### One curated list
|
||||
|
||||
`TEXT_ATTRS` is **gone** — prop types now decide what counts as a text prop, so there
|
||||
is no list to maintain (§8). Declaring a prop `I18nText` is what makes it enforced.
|
||||
|
||||
- **`TEXT_ATTRS`** (47 names) — props that carry user-facing text. Feeds both the
|
||||
static and bound rules, so "what counts as a text prop" is defined once. **Add a
|
||||
name here when a component takes UI text through a new prop.**
|
||||
- **`NON_TRANSLATABLE`** (39 entries) — tokens that must read identically in every
|
||||
language: units (`px`, `ms`, `ns`, `min`), symbols (`×`, `→`, `~`), protocol and
|
||||
spec identifiers (`GET`, `UTC`, `SQL`, `PromQL`, the OpenTelemetry statuses
|
||||
|
|
@ -121,7 +136,7 @@ follows a pattern the library already used for icons (`iconLeft?: IconName`).
|
|||
| **`I18nKey`** | a field holding an **i18n key as data** (`titleKey`, `labelKey`) |
|
||||
| **`useI18nTyped()`** | replaces `useI18n()` in components — `t` returns `I18nText` |
|
||||
| **`TranslateFn`** | the type of `t` when a composable/util takes it as a **parameter** |
|
||||
| **`gt()`** | last resort for genuinely context-free code — **1 file**, see below |
|
||||
| **`gt()`** | last resort for genuinely context-free code — **4 files**, see below |
|
||||
| **`raw()`** | the explicit opt-out for text that must not be translated |
|
||||
|
||||
```ts
|
||||
|
|
@ -155,7 +170,7 @@ presets.ts(76,5): error TS2820: Type '"emptyState.noLogs.titel"' is not assignab
|
|||
to type 'I18nKey'. Did you mean '"emptyState.noLogs.title"'?
|
||||
```
|
||||
|
||||
Cost: **~31 s** for the full app type-check with the 10,665-key union. Not a
|
||||
Cost: **~31 s** for the full app type-check with the ~9,800-key union. Not a
|
||||
perf concern.
|
||||
|
||||
### Getting a `t`: three cases, in order of preference
|
||||
|
|
@ -179,7 +194,7 @@ perf concern.
|
|||
}
|
||||
```
|
||||
|
||||
This is the standard for non-component code (**27 files**). It is preferred over
|
||||
This is the standard for non-component code (**35 files**). It is preferred over
|
||||
`gt()` because these functions are also called directly by specs, outside any
|
||||
component, where `useI18n()` would throw. Specs pass the real translator:
|
||||
|
||||
|
|
@ -192,13 +207,27 @@ perf concern.
|
|||
composable translates, `t` belongs on that function, not on the composable.
|
||||
|
||||
3. **Genuinely context-free code** → `gt("some.key")`. Only where there is neither a
|
||||
setup context nor a caller to thread `t` through. Exactly **one** file qualifies:
|
||||
`useUnauthorizedErrorGrouper.ts`, reached from an axios 403 interceptor registered
|
||||
once at module load (interceptor → 300 ms `setTimeout` → toast → click handler).
|
||||
setup context nor a caller to thread `t` through. Exactly **four** files qualify
|
||||
(10 call sites), each for a distinct structural reason:
|
||||
|
||||
- `useUnauthorizedErrorGrouper.ts` (6) — reached from an axios 403 interceptor
|
||||
registered once at module load (interceptor → 300 ms `setTimeout` → toast →
|
||||
click handler). The founding case.
|
||||
- `ingestion/setupCard/content/kubernetes.ts` (2) — the setup-card registry pins
|
||||
every builder to `(subs: CardSubstitutions) => RichCardContent`; threading `t`
|
||||
would change that shared contract for ~20 cards. Static prose in cards uses
|
||||
`descriptionKey`/`helpKey` (renderer-resolved); `gt` covers only the one
|
||||
description that interpolates a URL, translated at card-build time.
|
||||
- `utils/query/searchError.ts` (1) — the translated **default** of an optional
|
||||
`fallback: I18nText` parameter, evaluated per call; callers that hold `t`
|
||||
override it (e.g. `parseSearchError(err, gt("search.unknownError"))`).
|
||||
- `usePanelPromQLExecutor.ts` (1) — reached through composable chains that are
|
||||
not guaranteed to run in a setup context, where `useI18n()` would throw.
|
||||
|
||||
`gt` is deliberately a narrow escape hatch, not an alternative to case 2. Before
|
||||
reaching for it, check whether a caller can pass `t` instead — 43 of the original
|
||||
49 `gt` sites turned out to be case 2.
|
||||
reaching for it, check whether a caller can pass `t` instead — the overwhelming
|
||||
majority of historical `gt` sites (including the import validators and
|
||||
`useQualityDetailCharts`, converted since) turned out to be case 2.
|
||||
|
||||
> **Keys resolved at display time, not module load.** When storing keys as data, keep
|
||||
> the `I18nKey` map at module scope but resolve it inside the function that renders.
|
||||
|
|
@ -250,11 +279,18 @@ annotation is made by hand at the declaration rather than by a pattern match.
|
|||
| `t()` keys — literal | enforced (ESLint, `.vue` **and** `.ts`) |
|
||||
| i18n keys stored as data | enforced (`I18nKey`, 19 files) |
|
||||
| Toast / notification text | enforced (`I18nText`, incl. the store + wrappers) |
|
||||
| Text reached via `useI18nTyped()` | branded across **663** files |
|
||||
| Text reached via `useI18nTyped()` | branded across **668** files |
|
||||
| Composables / utils taking `t: TranslateFn` | **27** files |
|
||||
|
||||
**Numbers:** en-US grew 10,216 → **10,665 keys**; **311** sit under `toastMessages.*`
|
||||
(28 module groups). 62 `raw()` opt-outs. **6** `gt()` call sites, all in one file.
|
||||
**Numbers** (re-measured 2026-07-31, after the post-review debt cleanup): en-US holds
|
||||
**9,837 keys** across **72 namespaces** (10,216 → 11,002 as text was migrated in,
|
||||
→ 9,648 after the dead-key sweep §9, → 9,837 as the remaining hardcoded prose —
|
||||
import validators, setup-card descriptions, status labels, dialog buttons — was
|
||||
keyed); **309** sit under `toastMessages.*`. **977** `raw()` opt-outs — the count
|
||||
rose steeply when props became `I18nText`, since every genuinely non-translatable
|
||||
value (units, tokens, glyphs, API data) has to say so explicitly, then fell as
|
||||
`raw()`-wrapped prose was converted to keys. **10** `gt()` call sites in **4**
|
||||
files (see §3 case 3 for the per-file justification).
|
||||
|
||||
**Where the two checkers divide.** `no-missing-keys` validates `t('x.y')` **calls**;
|
||||
it cannot see a key assigned to a field. Keys stored as **data** are validated instead
|
||||
|
|
@ -264,27 +300,54 @@ be green to claim key coverage; neither alone is sufficient.
|
|||
|
||||
### Not covered
|
||||
|
||||
- **Dynamic keys** — ``t(`about.feature_${id}`)`` (**312** sites). No lint or type
|
||||
check can resolve these. The `t` key parameter is deliberately permissive
|
||||
(`I18nKey | (string & {})`) so they keep compiling.
|
||||
- **Dynamic keys — 71 sites. Considered and DECLINED; do not re-open as pending work.**
|
||||
|
||||
``t(`about.feature_${id}`)`` and similar. A renamed or deleted key reached this way is
|
||||
caught by nothing: ESLint sees no literal, `t`'s permissive
|
||||
`I18nKey | (string & {})` parameter accepts any string, vue-i18n returns the key
|
||||
instead of throwing, and the production build strips its dev warning. It renders the
|
||||
raw dotted path on screen.
|
||||
|
||||
Nothing is currently broken — every key these sites can request was expanded and
|
||||
verified present. The exposure is a future rename, concentrated in `onlineEvals`
|
||||
(40 of the 71 sites).
|
||||
|
||||
Closing it would mean removing the `(string & {})` arm, which surfaces **117 errors**:
|
||||
~80 are unrelated consumers declaring their translator loosely as
|
||||
`(key: string) => string` (a contravariance failure, not a key problem), 15 need a
|
||||
variable widened to a literal union, 9 need a `Record<string, I18nKey>` map, and a
|
||||
handful are genuine key/code mismatches. Two-thirds of the work is therefore not about
|
||||
dynamic keys at all, which is why this was judged not worth it as a standalone project.
|
||||
|
||||
If the guarantee is ever wanted, the cheap route is a second strict export
|
||||
(`(key: I18nKey)`) used by new code only — no flag day, no 98-file PR.
|
||||
|
||||
> Count these with a word boundary: `grep 't(\`'` also matches `` fetch(`…`) `` and any
|
||||
other call ending in `t`, which inflates the figure roughly fourfold. A further 21
|
||||
> sites use backticks but interpolate nothing — those are ordinary static keys.
|
||||
|
||||
- **Interfaces not yet annotated.** `I18nText` guards what it is applied to. That is
|
||||
_incomplete_, never _wrong_ — coverage grows one declaration at a time, at the
|
||||
definition site, with no central registry to keep in sync.
|
||||
- **Other locales.** The 14 non-en locales are generated from en-US and lag behind;
|
||||
`localeDir` points at en-US only, on purpose. Never hand-edit them.
|
||||
- **Unused keys.** `@intlify/vue-i18n/no-unused-keys` is available but **not
|
||||
enabled**. Measured on this repo it reports 2,421 keys of which only ~1,302 are
|
||||
genuinely dead — 681 are reached via a key-string, 437 via a dynamic prefix. If
|
||||
you enable it, use `warn`, never `enableFix`, and populate `ignores` with the
|
||||
dynamic prefixes; its autofix would delete live translations.
|
||||
- **Unused keys.** `@intlify/vue-i18n/no-unused-keys` is available but **not enabled**.
|
||||
It reports 2,421 keys, but it only sees `t('literal')` calls — so keys reached
|
||||
through a variable or a dynamic prefix look unused to it, and its autofix would
|
||||
delete them. A direct measurement (literal tokens + dynamic prefixes + the one
|
||||
concatenation site) put the genuinely dead set at **1,363**, since deleted (§9).
|
||||
If you ever enable the rule, use `warn`, never `enableFix`, and populate `ignores`
|
||||
with the dynamic prefixes. Note that even the direct measurement missed 9 live keys
|
||||
(§9) — no automated sweep sees keys held in JSON data files.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deliberately deferred: `strictTemplates`
|
||||
|
||||
Typing a component prop `label: I18nText` only gates `<OButton label="Save" />` if
|
||||
Vue's `strictTemplates` is on. It is **not** enabled, which is why `TEXT_ATTRS`
|
||||
still exists.
|
||||
A **declared** prop typed `label: I18nText` gates `<OButton label="Save" />` with or
|
||||
without `strictTemplates` — declared props are always checked. `strictTemplates`
|
||||
governs **undeclared** attributes only. This was verified by probe, and it is what
|
||||
allowed `TEXT_ATTRS` to be retired (§8) while `strictTemplates` stays off.
|
||||
|
||||
Measured twice on this branch:
|
||||
|
||||
|
|
@ -297,10 +360,9 @@ Of those, ~303 are genuine `TS2322` type mismatches (real latent bugs); the rest
|
|||
undeclared pass-through attributes.
|
||||
|
||||
**It was left out on purpose.** It is a large _type-safety_ migration, not i18n work,
|
||||
and its i18n payoff is only retiring the `TEXT_ATTRS` list — whose real gap was
|
||||
measured at **five sites** (all fixed here: `reveal-tooltip`, `hide-tooltip`,
|
||||
`unstable-dimension-tooltip`, `date-disabled-tooltip`). Worth doing as its own PR;
|
||||
not worth burying this one under 2,655 unrelated errors.
|
||||
and it turned out to carry **no i18n payoff at all** — retiring `TEXT_ATTRS` did not
|
||||
require it. Worth doing as its own PR for the ~303 genuine `TS2322` mismatches; not
|
||||
worth burying this one under 2,655 unrelated errors.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -320,8 +382,17 @@ not worth burying this one under 2,655 unrelated errors.
|
|||
| A recurring unit/symbol/code token | add to `NON_TRANSLATABLE` with a one-line reason |
|
||||
| A whole file that is genuinely code | add to the SyntaxGuide exemption block |
|
||||
|
||||
New text-carrying **component prop** → add its name to `TEXT_ATTRS`.
|
||||
New text-carrying **component prop** → declare it `I18nText`. In `<script setup>`,
|
||||
`defineProps<{ label: I18nText }>()`. In the Options API the double cast is required,
|
||||
because `StringConstructor` resolves to an unbranded `string`:
|
||||
|
||||
```ts
|
||||
title: { type: String as unknown as PropType<I18nText>, default: raw("") }
|
||||
```
|
||||
|
||||
New **interface field** carrying text or a key → declare it `I18nText` / `I18nKey`.
|
||||
New **i18n key stored in a `.json` data file** → add it to the `localeKeys.spec.ts`
|
||||
guard; no type or lint rule can see it (§9).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -397,10 +468,25 @@ Recorded so they are not re-reported as open:
|
|||
- **Verb injection.** 5 messages interpolated an English verb into a sentence
|
||||
(`"Failed to {action} the alert"`), which is unlocalisable word order; each is now a
|
||||
complete message per case.
|
||||
- **`gt` proliferation.** 49 sites / 22 files → **6 sites / 1 file**, by passing
|
||||
`t: TranslateFn` (§3, case 2).
|
||||
- **`gt` proliferation.** 49 sites / 22 files → **10 sites / 4 files**, by passing
|
||||
`t: TranslateFn` (§3, case 2). The audit (2026-07-31) converted every site whose
|
||||
caller could supply `t` — the onlineEvals import validators now take `t` via their
|
||||
`ctx`, `useQualityDetailCharts` and `computePrefixAssignment` take it as a
|
||||
parameter. The 10 that remain are each structurally unable to receive a `t`
|
||||
(axios interceptor, fixed registry contract, optional-param default, non-setup
|
||||
composable chain — the per-file list is in §3 case 3) and are **intentional and
|
||||
final**. Two alternatives were considered and rejected: deferring keys to render
|
||||
time, and injecting a translator at the composition root. `gt` stays exported as
|
||||
a deliberate, narrow escape hatch; **do not re-raise this as cleanup.**
|
||||
- **The `useI18n` import ban** is clean tree-wide. The only remaining `vue-i18n` import
|
||||
outside the exempt paths is `createI18n` in a test fixture, which the ban permits.
|
||||
- **`TEXT_ATTRS` is gone.** The 47-name attribute allowlist and the bound-prop half of
|
||||
`local/no-bare-bound-text-props` were deleted; prop types now do that job (§3). A
|
||||
declared `I18nText` prop is checked in templates **without** `strictTemplates` —
|
||||
`strictTemplates` governs _undeclared_ attributes only, so §5 is not a blocker for
|
||||
this. Type checking is also _stricter_ than the old rule: it rejects a plain `string`
|
||||
variable, which the rule allowed. `vue/no-bare-strings-in-template` stays enabled,
|
||||
because a bare text node has no prop to annotate; `NON_TRANSLATABLE` still feeds it.
|
||||
- **Fake plurals everywhere else.** The remaining 104 `(s)` messages are gone — pipe
|
||||
plurals went 53 → **139**. Breakdown of how they were fixed:
|
||||
- 69 already interpolated `{count}`/`{n}`, so the call sites were already correct and
|
||||
|
|
@ -424,9 +510,50 @@ Recorded so they are not re-reported as open:
|
|||
|
||||
## 9. Outstanding work
|
||||
|
||||
Ordered by value. Item 3 is the cheapest; item 1 is the only user-visible one.
|
||||
### ~~1. Delete dead keys from `en-US.json`~~ — done
|
||||
|
||||
### 1. Non-English plural rules (`pluralRules`)
|
||||
`en-US.json` went from **11,002 → 9,648 keys** (75 → 71 namespaces, ~90 KB smaller).
|
||||
1,363 keys were deleted and **9 were restored** after audit; see below. (The count
|
||||
has since grown again to **9,837 / 72 namespaces** as the post-review debt cleanup
|
||||
keyed the remaining hardcoded prose — that growth is live keys, not resurrected
|
||||
dead ones.)
|
||||
|
||||
**What the scan excluded up front.** Keys with a literal reference in prod code or
|
||||
specs, keys reachable through a dynamic prefix (a conservative exclusion — spared
|
||||
because a dynamic key _might_ reach them, not because they are proven live), and the
|
||||
5 keys reachable through the one concatenation site (`utils/common.ts`, `"message." +
|
||||
code`).
|
||||
|
||||
**The audit that followed, and why it mattered.** Six independent read-only agents
|
||||
re-checked all 1,363 deletions, each told to hunt specifically for what a
|
||||
quoted-literal scan structurally cannot see. They found **9 genuine false deletions**
|
||||
across two distinct blind spots — both now closed by guards that were verified to
|
||||
fail when the key is removed:
|
||||
|
||||
| Blind spot | Keys | Guard added |
|
||||
| ----------------------------------------------------------------- | ---- | ------------------------------------------------------------- |
|
||||
| Keys living in a **JSON data file** (`constants/features.json`) | 5 | `localeKeys.spec.ts` — resolves every key features.json cites |
|
||||
| Template literal **assigned to a variable** before reaching `t()` | 4 | `useGreeting.ts` — `const key: I18nKey = …` |
|
||||
|
||||
The first is the more important lesson: TypeScript widens imported JSON string values
|
||||
to `string`, so keys stored in a data file cannot be typed as `I18nKey` and are
|
||||
invisible to _every_ static check. Annotating `FeatureAvailability` documents intent
|
||||
but **does not enforce** — `FEATURE_REGISTRY` casts with `as`, and the widening defeats
|
||||
it regardless. Only the spec guard actually catches this.
|
||||
|
||||
**If you delete keys again.** Regenerate the list (the set moves as code changes) and
|
||||
scan **`.json`/`.md`/`.yaml` as well as `.ts`/`.vue`** — the original sweep read only
|
||||
JS/TS and that is exactly how the features.json keys were lost. Never use
|
||||
`no-unused-keys --fix`: it shares the blind spot and would delete the dynamic-reachable
|
||||
keys. `no-missing-keys` and `I18nKey` together catch an over-delete of any _literal_
|
||||
reference, but neither sees the two cases above.
|
||||
|
||||
**Other locales.** The 14 non-English files still carry ~1,317 of the deleted keys.
|
||||
This is expected and self-healing: `scripts/translations/README.md` documents the
|
||||
pipeline as pruning keys removed from `en-US.json`. Harmless meanwhile — nothing
|
||||
requests them and en-US is the fallback.
|
||||
|
||||
### 2. Non-English plural rules (`pluralRules`)
|
||||
|
||||
The en-US messages are now real pipe plurals, but **branch selection still uses
|
||||
vue-i18n's built-in default for every locale** because this repo configures no
|
||||
|
|
@ -444,26 +571,47 @@ generated and must not be hand-edited, so they keep their `(s)` until regenerate
|
|||
This degrades safely: if en-US has a pipe and fr-FR does not, vue-i18n returns the
|
||||
French message unchanged rather than mis-selecting a branch.
|
||||
|
||||
### 2. The last 6 `gt()` sites
|
||||
### 3. Spec type-checking does not exist — anywhere
|
||||
|
||||
`useUnauthorizedErrorGrouper.ts`. Blocked on architecture, not effort — see §3 case 3
|
||||
for why there is no caller to thread `t` through. Two designs were considered and
|
||||
rejected (deferring keys to render time; injecting a translator at the composition
|
||||
root). Closing this is what would allow `gt` to be deleted from `types/i18n.ts`
|
||||
entirely.
|
||||
Two separate problems compound here.
|
||||
|
||||
### 3. Spec-inclusive `type-check` does not gate PRs
|
||||
**First, the config is broken.** `tsconfig.vitest.json`'s include is
|
||||
`src/**/*.spec.{ts,js}` — but TypeScript globs do **not** support brace expansion,
|
||||
so the pattern matches **zero files**. `npm run type-check` "passes" in seconds
|
||||
because it checks nothing but `vitest.config.ts`/`vite.config.ts` (verify with
|
||||
`npx tsc -p tsconfig.vitest.json --listFilesOnly`). Any past claim that the tree
|
||||
is "green under both configs" was vacuous. Additionally, the npm script's
|
||||
`NODE_OPTIONS=…` prefix is POSIX-only, so `npm run type-check` errors outright on
|
||||
Windows shells.
|
||||
|
||||
`unit-tests.yml` runs `type-check:app`, which **excludes specs**. The spec-inclusive
|
||||
`type-check` runs only in `npm-update.yml`, a scheduled dependency job — so a spec that
|
||||
fails to compile can reach `main`. Adding one step to `unit-tests.yml` closes it, and
|
||||
the tree is currently green under both, so it would go in clean.
|
||||
**Second, even a working spec type-check would not gate PRs.** `unit-tests.yml`
|
||||
(the PR gate) runs only `type-check:app`, which excludes specs. Vitest transpiles
|
||||
through esbuild, which strips types without checking them, so a type-broken spec
|
||||
still runs and can pass while testing the wrong thing. This branch hit exactly
|
||||
that — when `useDashboardPanelData()` gained its `t` parameter, **55 specs** kept
|
||||
the old signature while `type-check:app` reported 0 errors.
|
||||
|
||||
Fix is two steps, in order:
|
||||
|
||||
1. Split the include in `tsconfig.vitest.json` into `src/**/*.spec.ts` and
|
||||
`src/**/*.spec.js`, run it, and fix what surfaces — spec-only type errors exist
|
||||
today (e.g. `metricGrouping.spec.ts` assigns non-existent keys to `I18nKey`
|
||||
fixtures), so expect this to start **red**, not clean.
|
||||
2. Then add the step next to the existing one in `unit-tests.yml`:
|
||||
|
||||
```yaml
|
||||
- name: Type-check specs (any error fails)
|
||||
working-directory: web
|
||||
run: npm run type-check
|
||||
```
|
||||
|
||||
### 4. Standing limits (accepted, not scheduled)
|
||||
|
||||
- **312 dynamic keys** — unresolvable by design (§4).
|
||||
- **`no-unused-keys` not enabled** — ~1,302 genuinely dead keys of the 2,421 it
|
||||
reports; its autofix would delete live translations (§4).
|
||||
- **71 dynamic keys** — conversion considered and **declined**; see §4 for the full
|
||||
reasoning and what closing it would cost.
|
||||
- **`no-unused-keys` not enabled** — it reports 2,421 keys but shares the blind spot
|
||||
that makes ~1,000 of those false positives, and its autofix deletes what it flags.
|
||||
The dead-key cleanup in §9.1 uses a directly-measured list instead (§4).
|
||||
- **`strictTemplates` deferred** — 2,655 errors, a type-safety migration rather than
|
||||
i18n work (§5).
|
||||
- **`eslint.config.js` duplicate rule keys** — `prettier/prettier` appears 3×,
|
||||
|
|
|
|||
|
|
@ -73,68 +73,6 @@ const noLegacyO2Tokens = {
|
|||
},
|
||||
};
|
||||
|
||||
// User-facing text props. `vue/no-bare-strings-in-template` checks these as STATIC
|
||||
// attributes (label="Save"); the custom rule below checks the SAME names as BOUND
|
||||
// literals (:label="'Save'" / :label=`Save`). One list feeds both, so "what counts
|
||||
// as translatable text passed to a component" is defined in exactly one place —
|
||||
// add a prop name here when a component takes user-facing text through a new prop.
|
||||
// The a11y/native entries reproduce the built-in rule's defaults (we replace, not
|
||||
// extend, its attribute map). Component props are evidence-based (scan of web/src).
|
||||
const TEXT_ATTRS = [
|
||||
"title",
|
||||
"aria-label",
|
||||
"aria-placeholder",
|
||||
"aria-roledescription",
|
||||
"aria-valuetext",
|
||||
"alt",
|
||||
"label",
|
||||
"sub-label",
|
||||
"sublabel",
|
||||
"placeholder",
|
||||
"hint",
|
||||
"tooltip",
|
||||
"message",
|
||||
"content",
|
||||
"help-text",
|
||||
"caption",
|
||||
"description",
|
||||
"subtitle",
|
||||
"header",
|
||||
"empty-label",
|
||||
"error-message",
|
||||
"button-label",
|
||||
"primary-button-label",
|
||||
"secondary-button-label",
|
||||
"confirm-text",
|
||||
"cancel-text",
|
||||
// Additional O2/app text props, discovered by scanning web/src for every
|
||||
// `:prop="t(...)"` — a prop a dev already wraps in t() is by definition
|
||||
// translatable text, so its STATIC / bound-literal form must be guarded too.
|
||||
// Re-run that scan when adding text-carrying props and keep this in sync.
|
||||
"sub-title",
|
||||
"footer-title",
|
||||
"dirty-title",
|
||||
"action-label",
|
||||
"ok-label",
|
||||
"firing-label",
|
||||
"neutral-button-label",
|
||||
"filter-label",
|
||||
"empty-message",
|
||||
"no-match-text",
|
||||
"search-placeholder",
|
||||
"ai-placeholder",
|
||||
"ai-tooltip",
|
||||
"disable-ai-reason",
|
||||
"full-time-prefix",
|
||||
"legend-healthy",
|
||||
"legend-avg",
|
||||
"reveal-tooltip",
|
||||
"hide-tooltip",
|
||||
"unstable-dimension-tooltip",
|
||||
"date-disabled-tooltip",
|
||||
];
|
||||
const TEXT_ATTR_SET = new Set(TEXT_ATTRS);
|
||||
|
||||
// Non-translatable literal tokens — code/syntax/units that must stay identical in
|
||||
// every language (a CSS unit, a fixed filename, a documented template-variable token).
|
||||
// Fed to BOTH i18n rules so they pass WITHOUT a scattered inline eslint-disable
|
||||
|
|
@ -224,10 +162,12 @@ const BARE_STRING_DEFAULT_ALLOWLIST = [
|
|||
|
||||
// Bans hardcoded text left directly in a <template> that the built-in
|
||||
// `vue/no-bare-strings-in-template` (STATIC attrs + text nodes only) can't see:
|
||||
// • a BOUND text prop — :label="'Save'" / :label=`Go`
|
||||
// • a v-text / v-html literal — v-text="'Save'"
|
||||
// • a text interpolation — {{ 'Save' }}
|
||||
// so a dev can't dodge the check by adding a `:`, a v-text, or mustaches. t()-bound
|
||||
// so a dev can't dodge the check with a v-text or mustaches. (Bound text PROPS are
|
||||
// no longer checked here — every text prop is declared I18nText, which rejects a
|
||||
// bare literal, a composed expression, AND a plain string variable at type-check.)
|
||||
// t()-bound
|
||||
// and variable-bound values are expressions (not bare literals) so they correctly
|
||||
// pass; a literal with no letters (punctuation like '—') is skipped. Only BARE
|
||||
// literals are caught — composed expressions (:label="'a'+b", ternaries, `${x} y`)
|
||||
|
|
@ -298,12 +238,10 @@ noLegacyO2Tokens.rules["no-bare-bound-text-props"] = {
|
|||
const text = bareText(node.value && node.value.expression);
|
||||
if (text == null) return;
|
||||
if (dir === "bind") {
|
||||
const arg = node.key.argument; // only :prop / v-bind:prop in the text-attr set
|
||||
if (!arg || arg.type !== "VIdentifier" || !TEXT_ATTR_SET.has(arg.name)) return;
|
||||
context.report({
|
||||
node,
|
||||
message: `Hardcoded text "${text}" in bound prop :${arg.name} — use t('...') with a key in en-US.json.`,
|
||||
});
|
||||
// Bound text props are now guarded by the TYPE, not by a name list: a
|
||||
// prop declared `I18nText` rejects a bare literal (and a plain string
|
||||
// variable) at type-check. See src/types/i18n.ts.
|
||||
return;
|
||||
} else if (dir === "text" || dir === "html") {
|
||||
context.report({
|
||||
node,
|
||||
|
|
@ -399,19 +337,24 @@ export default [
|
|||
"@intlify/vue-i18n/no-missing-keys": "error",
|
||||
//
|
||||
// `no-bare-strings-in-template` (ERROR): no user-facing string typed straight
|
||||
// into a <template> — text nodes AND static text props (label="Save"). We
|
||||
// REPLACE the built-in attribute map with TEXT_ATTRS so component props, not
|
||||
// just native title/alt/placeholder, are covered. Bound props (:label="'x'")
|
||||
// are caught by the local rule below. (@intlify's own `no-raw-text` is NOT
|
||||
// used — it flags ~1800 literals/punctuation and is too noisy to gate.)
|
||||
// into a <template> TEXT NODE. (@intlify's own `no-raw-text` is NOT used —
|
||||
// it flags ~1800 literals/punctuation and is too noisy to gate.)
|
||||
//
|
||||
// Props are NOT listed here any more. Every text-carrying prop is declared
|
||||
// `I18nText` (src/types/i18n.ts), so `label="Save"`, `:label="'Save'"`,
|
||||
// composed expressions, and even a plain `string` variable are all rejected
|
||||
// by `type-check:app` — strictly more than the old hand-maintained name list
|
||||
// could catch, and with nothing to keep in sync. A text NODE has no prop to
|
||||
// annotate, which is why this rule still runs.
|
||||
"vue/no-bare-strings-in-template": [
|
||||
"error",
|
||||
{
|
||||
attributes: { "/.+/": TEXT_ATTRS },
|
||||
attributes: {},
|
||||
allowlist: [...BARE_STRING_DEFAULT_ALLOWLIST, ...NON_TRANSLATABLE],
|
||||
},
|
||||
],
|
||||
// The bound-prop half of the same rule (see TEXT_ATTRS above).
|
||||
// Text-position interpolation `{{ 'Save' }}` and v-text/v-html only; the
|
||||
// bound-prop half retired with TEXT_ATTRS (the type covers it).
|
||||
"local/no-bare-bound-text-props": "error",
|
||||
//
|
||||
// `t` must come from the typed wrapper, otherwise it returns an unbranded
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:class="isAnimating ? 'auto-refresh-icon--spinning' : ''"
|
||||
size="sm"
|
||||
/>
|
||||
<OTooltip :content="`${t('search.autoRefresh')}: ${selectedLabel}`" />
|
||||
<OTooltip :content="raw(`${t('search.autoRefresh')}: ${selectedLabel}`)" />
|
||||
</OButton>
|
||||
</template>
|
||||
<div class="w-75 p-2">
|
||||
|
|
@ -76,7 +76,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
side="right"
|
||||
align="center"
|
||||
max-width="18.75rem"
|
||||
:content="minRangeRestrictionMessageVal"
|
||||
:content="raw(minRangeRestrictionMessageVal)"
|
||||
/>
|
||||
{{ item.label }}
|
||||
</OButton>
|
||||
|
|
@ -152,7 +152,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
side="right"
|
||||
align="center"
|
||||
max-width="18.75rem"
|
||||
:content="minRangeRestrictionMessageVal"
|
||||
:content="raw(minRangeRestrictionMessageVal)"
|
||||
/>
|
||||
{{ item.label }}
|
||||
</OButton>
|
||||
|
|
@ -174,7 +174,7 @@ import {
|
|||
onMounted,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { generateDurationLabel } from "../utils/date";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
@ -362,6 +362,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
router,
|
||||
btnRefreshInterval,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import {
|
|||
onActivated,
|
||||
watch,
|
||||
computed,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
|
||||
import type * as MonacoEditor from "monaco-editor/esm/vs/editor/editor.api";
|
||||
|
|
@ -86,7 +87,7 @@ import { useTheme } from "@/composables/useTheme";
|
|||
import { debounce } from "lodash-es";
|
||||
import searchState from "@/composables/useLogs/searchState";
|
||||
import { useNLQuery } from "@/composables/useNLQuery";
|
||||
import { useI18nTyped, raw } from "@/types/i18n";
|
||||
import { type I18nText, useI18nTyped, raw } from "@/types/i18n";
|
||||
import useNotifications from "@/composables/useNotifications";
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
import { isAuthError } from "@/utils/authErrors";
|
||||
|
|
@ -168,8 +169,8 @@ export default defineComponent({
|
|||
default: false,
|
||||
},
|
||||
disableAiReason: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
data-test="confirm-dialog-provider"
|
||||
size="sm"
|
||||
:title="currentDialog.title"
|
||||
:primary-button-label="currentDialog.confirmLabel || t('common.ok')"
|
||||
:secondary-button-label="currentDialog.cancelLabel || t('common.cancel')"
|
||||
:primary-button-label="raw(currentDialog.confirmLabel) || t('common.ok')"
|
||||
:secondary-button-label="raw(currentDialog.cancelLabel) || t('common.cancel')"
|
||||
:persistent="currentDialog.persistent ?? true"
|
||||
@click:primary="handleConfirm"
|
||||
@click:secondary="handleCancel"
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<script setup lang="ts">
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
import { useConfirmDialog } from "@/composables/useConfirmDialog";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
|
||||
const { currentDialog, handleConfirm, handleCancel, handleUpdateOpen } = useConfirmDialog();
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,14 @@ const mockStore = createStore({
|
|||
const mockI18n = createI18n({
|
||||
locale: "en",
|
||||
messages: {
|
||||
en: {},
|
||||
// The component passes t("common.*") messages to copyToClipboard, so
|
||||
// mounts using this mock need the keys or t() echoes the key path.
|
||||
en: {
|
||||
common: {
|
||||
contentCopiedSuccessfully: "Content Copied Successfully!",
|
||||
copyContentError: "Error while copying content.",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -119,7 +126,7 @@ describe("CopyContent.vue Branch Coverage", () => {
|
|||
expect.any(Function),
|
||||
{
|
||||
successMessage: "Content Copied Successfully!",
|
||||
errorMessage: "Error while copy content.",
|
||||
errorMessage: "Error while copying content.",
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
|
|
@ -153,7 +160,7 @@ describe("CopyContent.vue Branch Coverage", () => {
|
|||
expect.any(Function),
|
||||
{
|
||||
successMessage: "Content Copied Successfully!",
|
||||
errorMessage: "Error while copy content.",
|
||||
errorMessage: "Error while copying content.",
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
|
|
@ -323,7 +330,7 @@ describe("CopyContent.vue Branch Coverage", () => {
|
|||
expect.any(Function),
|
||||
{
|
||||
successMessage: "Content Copied Successfully!",
|
||||
errorMessage: "Error while copy content.",
|
||||
errorMessage: "Error while copying content.",
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { defineComponent, ref, type PropType } from "vue";
|
||||
import { type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import { maskText, b64EncodeStandard } from "../utils/zincutils";
|
||||
|
|
@ -51,7 +51,7 @@ export default defineComponent({
|
|||
components: { OButton, OIcon, OTooltip },
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: "", // Default value for content prop (empty string in this case)
|
||||
},
|
||||
displayContent: {
|
||||
|
|
@ -88,8 +88,8 @@ export default defineComponent({
|
|||
const copyToClipboardFn = () => {
|
||||
const content = replaceValues(props.content, false);
|
||||
copyToClipboard(content, t, {
|
||||
successMessage: "Content Copied Successfully!",
|
||||
errorMessage: "Error while copy content.",
|
||||
successMessage: t("common.contentCopiedSuccessfully"),
|
||||
errorMessage: t("common.copyContentError"),
|
||||
timeout: 5000,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
|
|||
import type { DropdownAlign } from "@/lib/overlay/Dropdown/ODropdown.types";
|
||||
import { ref, reactive, watch, computed } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ const { t } = useI18nTyped();
|
|||
type PeriodKey = "s" | "m" | "h" | "d" | "w" | "M";
|
||||
|
||||
interface RelativePeriod {
|
||||
label: string;
|
||||
label: I18nText;
|
||||
value: PeriodKey;
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ const picker = reactive({
|
|||
relative: {
|
||||
value: 0,
|
||||
period: "m",
|
||||
label: "Minutes",
|
||||
label: t("common.minutes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -145,12 +145,12 @@ const picker = reactive({
|
|||
|
||||
// Periods for selection
|
||||
const relativePeriods: RelativePeriod[] = [
|
||||
{ label: "Seconds", value: "s" },
|
||||
{ label: "Minutes", value: "m" },
|
||||
{ label: "Hours", value: "h" },
|
||||
{ label: "Days", value: "d" },
|
||||
{ label: "Weeks", value: "w" },
|
||||
{ label: "Months", value: "M" },
|
||||
{ label: t("common.seconds"), value: "s" },
|
||||
{ label: t("common.minutes"), value: "m" },
|
||||
{ label: t("common.hours"), value: "h" },
|
||||
{ label: t("common.days"), value: "d" },
|
||||
{ label: t("common.weeks"), value: "w" },
|
||||
{ label: t("common.months"), value: "M" },
|
||||
];
|
||||
|
||||
const relativeDates: Record<PeriodKey, number[]> = {
|
||||
|
|
@ -164,18 +164,18 @@ const relativeDates: Record<PeriodKey, number[]> = {
|
|||
|
||||
// Options for custom period input
|
||||
const relativePeriodsSelect = ref([
|
||||
{ label: "Seconds", value: "s" },
|
||||
{ label: "Minutes", value: "m" },
|
||||
{ label: "Hours", value: "h" },
|
||||
{ label: "Days", value: "d" },
|
||||
{ label: "Weeks", value: "w" },
|
||||
{ label: "Months", value: "M" },
|
||||
{ label: t("common.seconds"), value: "s" },
|
||||
{ label: t("common.minutes"), value: "m" },
|
||||
{ label: t("common.hours"), value: "h" },
|
||||
{ label: t("common.days"), value: "d" },
|
||||
{ label: t("common.weeks"), value: "w" },
|
||||
{ label: t("common.months"), value: "M" },
|
||||
]);
|
||||
|
||||
// Function to map period values to their labels
|
||||
const getPeriodLabelFromValue = (periodValue: SelectModelValue | string) => {
|
||||
const period = relativePeriods.find((p) => p.value === periodValue);
|
||||
return period ? period.label : "Minutes";
|
||||
return period ? period.label : t("common.minutes");
|
||||
};
|
||||
|
||||
// Watch modelValue to reflect the correct offset when passed in from parent
|
||||
|
|
@ -212,13 +212,20 @@ const updateCustomPeriod = (newPeriod: SelectModelValue | string) => {
|
|||
emit("update:modelValue", `${picker.data.selectedDate.relative.value}${newPeriod}`);
|
||||
};
|
||||
|
||||
// Display the current selected offset
|
||||
// Display the current selected offset. Built from a parameterised message
|
||||
// rather than concatenation — word order around the value differs by language.
|
||||
const getDisplayValue = () => {
|
||||
return `${picker.data.selectedDate.relative.value} ${picker.data.selectedDate.relative.label} ago`;
|
||||
return t("common.relativeTimeAgo", {
|
||||
value: picker.data.selectedDate.relative.value,
|
||||
unit: picker.data.selectedDate.relative.label,
|
||||
});
|
||||
};
|
||||
|
||||
const getTrimmedDisplayValue = () => {
|
||||
return `Past ${picker.data.selectedDate.relative.value} ${picker.data.selectedDate.relative.label}`;
|
||||
return t("common.relativeTimePast", {
|
||||
value: picker.data.selectedDate.relative.value,
|
||||
unit: picker.data.selectedDate.relative.label,
|
||||
});
|
||||
};
|
||||
|
||||
// Check if the current selection matches the modelValue
|
||||
|
|
@ -234,7 +241,7 @@ const getPeriodLabel = () => {
|
|||
const selectedPeriod = relativePeriods.find(
|
||||
(p) => p.value === picker.data.selectedDate.relative.period,
|
||||
);
|
||||
return selectedPeriod ? selectedPeriod.label : "Minutes";
|
||||
return selectedPeriod ? selectedPeriod.label : t("common.minutes");
|
||||
};
|
||||
|
||||
const computedClass = computed(() => {
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
side="right"
|
||||
align="center"
|
||||
max-width="300px"
|
||||
:content="queryRangeRestrictionMsg"
|
||||
:content="raw(queryRangeRestrictionMsg)"
|
||||
/>
|
||||
</OButton>
|
||||
</div>
|
||||
|
|
@ -155,7 +155,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
align="center"
|
||||
max-width="300px"
|
||||
v-if="queryRangeRestrictionInHour > 0"
|
||||
:content="queryRangeRestrictionMsg"
|
||||
:content="raw(queryRangeRestrictionMsg)"
|
||||
/>
|
||||
|
||||
<div class="flex min-w-0 flex-1 gap-2">
|
||||
|
|
@ -195,7 +195,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
align="center"
|
||||
max-width="300px"
|
||||
v-if="queryRangeRestrictionInHour > 0"
|
||||
:content="queryRangeRestrictionMsg"
|
||||
:content="raw(queryRangeRestrictionMsg)"
|
||||
/>
|
||||
<div class="flex justify-center px-3 py-2">
|
||||
<ODateRangeCalendar
|
||||
|
|
@ -315,7 +315,7 @@ import {
|
|||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import { toast } from "@/lib/feedback/Toast/useToast";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
||||
|
||||
interface ConsumableDateTime {
|
||||
|
|
@ -466,7 +466,7 @@ export default defineComponent({
|
|||
const isTimezoneSelectOpen = ref(false);
|
||||
|
||||
const timezoneSelectOptions = computed(() =>
|
||||
timezoneOptions.map((tz: string) => ({ label: tz, value: tz })),
|
||||
timezoneOptions.map((tz: string) => ({ label: raw(tz), value: tz })),
|
||||
);
|
||||
|
||||
let relativePeriods = [
|
||||
|
|
@ -1283,6 +1283,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
menuOpen,
|
||||
onMenuOpenChange,
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, inject } from "vue";
|
||||
import { defineComponent, computed, inject, type PropType } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter, RouterLink } from "vue-router";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import { RailIndicatorActiveKey } from "@/lib/core/Navbar/ONavbar.types";
|
||||
|
||||
|
|
@ -80,13 +80,13 @@ export default defineComponent({
|
|||
components: { OIcon },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
caption: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
|
||||
link: {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
:sideOffset="8"
|
||||
side="bottom"
|
||||
align="center"
|
||||
:content="displayedTitle"
|
||||
:content="raw(displayedTitle)"
|
||||
/>
|
||||
</span>
|
||||
<OIcon name="arrow-drop-down" size="md" class="flex-shrink-0" />
|
||||
|
|
@ -220,7 +220,7 @@
|
|||
v-model:open="showImagePreview"
|
||||
@update:open="(v) => !v && closeImagePreview()"
|
||||
size="lg"
|
||||
:title="previewImage?.filename"
|
||||
:title="raw(previewImage?.filename)"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
|
|
@ -1018,7 +1018,7 @@
|
|||
class="rounded-default border-border-default max-h-37.5 max-w-50 cursor-pointer border object-contain [transition:transform_0.2s_ease,box-shadow_0.2s_ease] hover:scale-102 hover:shadow-[0_4px_12px_color-mix(in_srgb,var(--color-black)_15%,transparent)]"
|
||||
@click="openImagePreview(img)"
|
||||
/>
|
||||
<OTooltip :content="img.filename" />
|
||||
<OTooltip :content="raw(img.filename)" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(block, blockIndex) in message.blocks" :key="'fb-' + blockIndex">
|
||||
|
|
@ -1292,7 +1292,7 @@
|
|||
<RichTextInput
|
||||
ref="chatInput"
|
||||
v-model="inputMessage"
|
||||
:placeholder="inputPlaceholder"
|
||||
:placeholder="raw(inputPlaceholder)"
|
||||
:disabled="isLoading"
|
||||
:theme="store.state.theme"
|
||||
:references="contextReferences"
|
||||
|
|
@ -1391,7 +1391,7 @@ import {
|
|||
computed,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useTypewriterPlaceholder } from "@/components/ai-assistant/welcome/useTypewriterPlaceholder";
|
||||
import hljs from "highlight.js";
|
||||
|
|
@ -1652,7 +1652,7 @@ export default defineComponent({
|
|||
const pendingConfirmation = ref<{
|
||||
tool: string;
|
||||
args: Record<string, any>;
|
||||
message: string;
|
||||
message: I18nText;
|
||||
navAction?: NavigationAction;
|
||||
} | null>(null);
|
||||
|
||||
|
|
@ -1695,7 +1695,7 @@ export default defineComponent({
|
|||
// Active tool call state - for showing tool progress outside message box
|
||||
const activeToolCall = ref<{
|
||||
tool: string;
|
||||
message: string;
|
||||
message: I18nText;
|
||||
context: Record<string, any>;
|
||||
call_id?: string;
|
||||
} | null>(null);
|
||||
|
|
@ -2035,7 +2035,9 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
// Keep partial content but indicate it was cancelled
|
||||
lastMessage.content += "\n\n_[Response stopped by user]_";
|
||||
lastMessage.content = raw(
|
||||
lastMessage.content + "\n\n_[" + t("aiAssistant.responseStoppedByUser") + "]_",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2053,10 +2055,10 @@ export default defineComponent({
|
|||
// Tools ran before any text, so place them ahead of it.
|
||||
lastMessage.contentBlocks.unshift(...pendingToolCalls.value);
|
||||
} else {
|
||||
const stoppedNote = "_[Response stopped by user]_";
|
||||
const stoppedNote = `_[${t("aiAssistant.responseStoppedByUser")}]_`;
|
||||
chatMessages.value.push({
|
||||
role: "assistant",
|
||||
content: stoppedNote,
|
||||
content: raw(stoppedNote),
|
||||
contentBlocks: [...pendingToolCalls.value, { type: "text", text: stoppedNote }],
|
||||
});
|
||||
}
|
||||
|
|
@ -2217,7 +2219,7 @@ export default defineComponent({
|
|||
chatMessages.value = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Error: Unable to connect to backend",
|
||||
content: t("aiAssistant.backendConnectionError"),
|
||||
},
|
||||
];
|
||||
console.error("Error fetching initial message:", error);
|
||||
|
|
@ -2445,7 +2447,7 @@ export default defineComponent({
|
|||
} else {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
content: raw(""),
|
||||
contentBlocks: [...pendingToolCalls.value, confirmBlock],
|
||||
});
|
||||
pendingToolCalls.value = [];
|
||||
|
|
@ -2533,7 +2535,7 @@ export default defineComponent({
|
|||
if (!lastMessage || lastMessage.role !== "assistant") {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: errorMessage,
|
||||
content: raw(errorMessage),
|
||||
contentBlocks: [
|
||||
...pendingToolCalls.value,
|
||||
{ type: "text", text: errorMessage },
|
||||
|
|
@ -2543,9 +2545,9 @@ export default defineComponent({
|
|||
} else {
|
||||
// Append error to existing message
|
||||
if (lastMessage.content) {
|
||||
lastMessage.content += "\n\n" + errorMessage;
|
||||
lastMessage.content = raw(lastMessage.content + "\n\n" + errorMessage);
|
||||
} else {
|
||||
lastMessage.content = errorMessage;
|
||||
lastMessage.content = raw(errorMessage);
|
||||
}
|
||||
if (!lastMessage.contentBlocks) {
|
||||
lastMessage.contentBlocks = [];
|
||||
|
|
@ -2607,7 +2609,7 @@ export default defineComponent({
|
|||
} else {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
content: raw(""),
|
||||
contentBlocks: [...pendingToolCalls.value],
|
||||
});
|
||||
}
|
||||
|
|
@ -2772,7 +2774,7 @@ export default defineComponent({
|
|||
} else {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
content: raw(""),
|
||||
contentBlocks: [...pendingToolCalls.value, confirmBlock],
|
||||
});
|
||||
pendingToolCalls.value = [];
|
||||
|
|
@ -2835,7 +2837,7 @@ export default defineComponent({
|
|||
} else {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
content: raw(""),
|
||||
contentBlocks: [...pendingToolCalls.value, errorBlock],
|
||||
});
|
||||
pendingToolCalls.value = [];
|
||||
|
|
@ -2916,7 +2918,7 @@ export default defineComponent({
|
|||
// Create new assistant message with pending tool calls + text
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: streamingMsg,
|
||||
content: raw(streamingMsg),
|
||||
contentBlocks: [
|
||||
...pendingToolCalls.value,
|
||||
{ type: "text", text: textSegment },
|
||||
|
|
@ -2927,7 +2929,7 @@ export default defineComponent({
|
|||
await throttledSaveCtx(true);
|
||||
} else {
|
||||
// Update existing assistant message's total content
|
||||
lastMessage.content = streamingMsg;
|
||||
lastMessage.content = raw(streamingMsg);
|
||||
|
||||
// Update or add text block in contentBlocks
|
||||
if (!lastMessage.contentBlocks) {
|
||||
|
|
@ -3053,7 +3055,7 @@ export default defineComponent({
|
|||
if (!lastMessage || lastMessage.role !== "assistant") {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: errorMessage,
|
||||
content: raw(errorMessage),
|
||||
contentBlocks: [
|
||||
...pendingToolCalls.value,
|
||||
{ type: "text", text: errorMessage },
|
||||
|
|
@ -3062,9 +3064,9 @@ export default defineComponent({
|
|||
pendingToolCalls.value = [];
|
||||
} else {
|
||||
if (lastMessage.content) {
|
||||
lastMessage.content += "\n\n" + errorMessage;
|
||||
lastMessage.content = raw(lastMessage.content + "\n\n" + errorMessage);
|
||||
} else {
|
||||
lastMessage.content = errorMessage;
|
||||
lastMessage.content = raw(errorMessage);
|
||||
}
|
||||
if (!lastMessage.contentBlocks) {
|
||||
lastMessage.contentBlocks = [];
|
||||
|
|
@ -3215,7 +3217,7 @@ export default defineComponent({
|
|||
} else {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
content: raw(""),
|
||||
contentBlocks: [...pendingToolCalls.value, errorBlock],
|
||||
});
|
||||
pendingToolCalls.value = [];
|
||||
|
|
@ -3280,7 +3282,7 @@ export default defineComponent({
|
|||
if (!lastMessage || lastMessage.role !== "assistant") {
|
||||
msgs.push({
|
||||
role: "assistant",
|
||||
content: streamingMsg,
|
||||
content: raw(streamingMsg),
|
||||
contentBlocks: [
|
||||
...pendingToolCalls.value,
|
||||
{ type: "text", text: textSegment },
|
||||
|
|
@ -3289,7 +3291,7 @@ export default defineComponent({
|
|||
pendingToolCalls.value = [];
|
||||
await throttledSaveCtx(true);
|
||||
} else {
|
||||
lastMessage.content = streamingMsg;
|
||||
lastMessage.content = raw(streamingMsg);
|
||||
|
||||
if (!lastMessage.contentBlocks) {
|
||||
lastMessage.contentBlocks = [];
|
||||
|
|
@ -3481,7 +3483,7 @@ export default defineComponent({
|
|||
{
|
||||
id: "aiChatClose",
|
||||
key: "escape",
|
||||
description: "Close AI chat",
|
||||
description: t("shortcuts.actions.aiChatClose"),
|
||||
// Escape must close the chat even while typing a message in its input.
|
||||
allowInInput: true,
|
||||
handler: () => {
|
||||
|
|
@ -3496,7 +3498,7 @@ export default defineComponent({
|
|||
id: "aiChatExpand",
|
||||
key: "ctrl+b",
|
||||
keyForMac: "meta+b",
|
||||
description: "Expand/collapse AI chat",
|
||||
description: t("shortcuts.actions.aiChatExpand"),
|
||||
handler: toggleExpand,
|
||||
},
|
||||
]);
|
||||
|
|
@ -3762,10 +3764,14 @@ export default defineComponent({
|
|||
target.functionContent = vrlFunction;
|
||||
}
|
||||
|
||||
const streamLabel =
|
||||
{ logs: t("common.logs"), metrics: t("common.metrics"), traces: t("common.traces") }[
|
||||
streamType as string
|
||||
] ?? raw(streamType.charAt(0).toUpperCase() + streamType.slice(1));
|
||||
return {
|
||||
resource_type: streamType,
|
||||
action: "load_query",
|
||||
label: `View in ${streamType.charAt(0).toUpperCase() + streamType.slice(1)}`,
|
||||
label: t("aiAssistant.viewInTarget", { target: streamLabel }),
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
|
@ -3866,7 +3872,9 @@ export default defineComponent({
|
|||
return {
|
||||
resource_type: resourceType,
|
||||
action: "navigate_direct",
|
||||
label: `View ${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)}`,
|
||||
label: t("aiAssistant.viewTarget", {
|
||||
target: resourceType.charAt(0).toUpperCase() + resourceType.slice(1),
|
||||
}),
|
||||
target,
|
||||
};
|
||||
};
|
||||
|
|
@ -3989,22 +3997,22 @@ export default defineComponent({
|
|||
setTimeout(async () => {
|
||||
try {
|
||||
// Add success message AFTER navigation completes
|
||||
const successMessage = `Successfully navigated to ${pageName}`;
|
||||
const successMessage = t("aiAssistant.navigatedTo", { page: pageName });
|
||||
let lastMessage = chatMessages.value[chatMessages.value.length - 1];
|
||||
|
||||
if (!lastMessage || lastMessage.role !== "assistant") {
|
||||
// Create new assistant message
|
||||
chatMessages.value.push({
|
||||
role: "assistant",
|
||||
content: successMessage,
|
||||
content: raw(successMessage),
|
||||
contentBlocks: [{ type: "text", text: successMessage }],
|
||||
});
|
||||
} else {
|
||||
// Append to existing assistant message
|
||||
if (lastMessage.content) {
|
||||
lastMessage.content += "\n\n" + successMessage;
|
||||
lastMessage.content = raw(lastMessage.content + "\n\n" + successMessage);
|
||||
} else {
|
||||
lastMessage.content = successMessage;
|
||||
lastMessage.content = raw(successMessage);
|
||||
}
|
||||
if (!lastMessage.contentBlocks) {
|
||||
lastMessage.contentBlocks = [];
|
||||
|
|
@ -4161,7 +4169,7 @@ export default defineComponent({
|
|||
// But we'll use backendMessage for the API call
|
||||
chatMessages.value.push({
|
||||
role: "user",
|
||||
content: backendMessage, // Use backend message with full context
|
||||
content: raw(backendMessage), // Use backend message with full context
|
||||
...(hasImages && { images: messagesToSend }),
|
||||
});
|
||||
inputMessage.value = "";
|
||||
|
|
@ -4280,7 +4288,7 @@ export default defineComponent({
|
|||
}
|
||||
chatMessages.value.push({
|
||||
role: "assistant",
|
||||
content: errorMessage,
|
||||
content: raw(errorMessage),
|
||||
});
|
||||
await saveToHistory(); // Save after error
|
||||
}
|
||||
|
|
@ -5639,6 +5647,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
inputMessage,
|
||||
chatMessages,
|
||||
isLoading,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
@click="handleConfirm"
|
||||
@focus="handleYesFocus"
|
||||
@blur="handleYesBlur"
|
||||
>{{ confirmLabel }}</OButton
|
||||
>{{ resolvedConfirmLabel }}</OButton
|
||||
>
|
||||
<OButton
|
||||
ref="noButtonRef"
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
@click="handleCancel"
|
||||
@focus="handleNoFocus"
|
||||
@blur="handleNoBlur"
|
||||
>{{ cancelLabel }}</OButton
|
||||
>{{ resolvedCancelLabel }}</OButton
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -114,31 +114,34 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
||||
interface ConfirmationData {
|
||||
tool?: string;
|
||||
args?: Record<string, any>;
|
||||
message?: string;
|
||||
message?: I18nText;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
confirmation: ConfirmationData | null;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
confirmLabel?: I18nText;
|
||||
cancelLabel?: I18nText;
|
||||
}
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
confirmLabel: "Yes",
|
||||
cancelLabel: "No",
|
||||
confirmation: null,
|
||||
});
|
||||
|
||||
// Render-time defaults so the labels stay locale-reactive; a withDefaults
|
||||
// literal would freeze the English text.
|
||||
const resolvedConfirmLabel = computed(() => props.confirmLabel ?? t("common.yes"));
|
||||
const resolvedCancelLabel = computed(() => props.cancelLabel ?? t("common.no"));
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [];
|
||||
cancel: [];
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
-->
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useVirtualizer } from "@tanstack/vue-virtual";
|
||||
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
@ -39,7 +39,7 @@ import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
|
|||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
|
||||
interface OrgOption {
|
||||
label: string;
|
||||
label: I18nText;
|
||||
identifier: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import CodeQueryEditor from "@/components/CodeQueryEditor.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
@ -177,9 +177,9 @@ interface Props {
|
|||
editorHeight?: string;
|
||||
hideNlToggle?: boolean; // Hide floating AI icon (for pages that don't want AI)
|
||||
disableAi?: boolean; // Disable AI send (e.g. no stream selected)
|
||||
disableAiReason?: string; // Tooltip reason when AI is disabled
|
||||
aiPlaceholder?: string; // Custom placeholder for AI input (default: 'search.askAIPlaceholder')
|
||||
aiTooltip?: string; // Custom tooltip for AI send button (default: 'search.enterPrompt')
|
||||
disableAiReason?: I18nText; // Tooltip reason when AI is disabled
|
||||
aiPlaceholder?: I18nText; // Custom placeholder for AI input (default: 'search.askAIPlaceholder')
|
||||
aiTooltip?: I18nText; // Custom tooltip for AI send button (default: 'search.enterPrompt')
|
||||
hasExpandButton?: boolean; // Reserve right padding so AI bar close btn doesn't overlap the expand btn
|
||||
|
||||
// Testing
|
||||
|
|
@ -199,7 +199,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
editorHeight: "200px",
|
||||
hideNlToggle: false,
|
||||
disableAi: false,
|
||||
disableAiReason: "",
|
||||
disableAiReason: raw(""),
|
||||
hasExpandButton: false,
|
||||
dataTestPrefix: "query-editor",
|
||||
});
|
||||
|
|
@ -368,7 +368,7 @@ const handleAIGenerate = async () => {
|
|||
currentAbortController.value = new AbortController();
|
||||
|
||||
// Track user message for chat history
|
||||
chatMessages.value.push({ role: "user", content: userInput });
|
||||
chatMessages.value.push({ role: "user", content: raw(userInput) });
|
||||
|
||||
// Call the CodeQueryEditor's handleGenerateSQL method with abort + session
|
||||
if (editorRef.value && typeof editorRef.value.handleGenerateSQL === "function") {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
|
||||
<div v-else-if="error" class="p-4">
|
||||
<OBanner variant="error" icon="error" :content="error" />
|
||||
<OBanner variant="error" icon="error" :content="raw(error)" />
|
||||
</div>
|
||||
|
||||
<!-- EXPLAIN ANALYZE view -->
|
||||
|
|
@ -180,7 +180,7 @@ import OIcon from "@/lib/core/Icon/OIcon.vue";
|
|||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
import { defineComponent, ref, computed, watch } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import streamingSearch from "@/services/streaming_search";
|
||||
import { useSearchStream } from "@/composables/useLogs/useSearchStream";
|
||||
|
|
@ -596,6 +596,7 @@ export default defineComponent({
|
|||
);
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
showDialog,
|
||||
loading,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
class="rich-text-input text-text-body relative max-h-75 min-h-10 overflow-y-auto text-sm leading-[1.6] break-words whitespace-pre-wrap outline-none"
|
||||
:class="disabled ? 'cursor-not-allowed' : ''"
|
||||
contenteditable="true"
|
||||
:data-placeholder="placeholder"
|
||||
:data-placeholder="resolvedPlaceholder"
|
||||
@input="handleInput"
|
||||
@keydown="handleKeyDown"
|
||||
@paste="handlePaste"
|
||||
|
|
@ -42,7 +42,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, watch, nextTick, PropType } from "vue";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { computed, defineComponent, ref, onMounted, watch, nextTick, PropType } from "vue";
|
||||
|
||||
export interface ReferenceChip {
|
||||
id: string;
|
||||
|
|
@ -63,8 +64,9 @@ export default defineComponent({
|
|||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "Write your prompt",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
// Resolved in setup so the fallback is translated, not frozen at load.
|
||||
default: undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
|
@ -87,6 +89,8 @@ export default defineComponent({
|
|||
},
|
||||
emits: ["update:modelValue", "keydown", "submit", "focus", "blur", "update:references"],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18nTyped();
|
||||
const resolvedPlaceholder = computed(() => props.placeholder ?? t("common.writeYourPrompt"));
|
||||
const editableDiv = ref<HTMLDivElement | null>(null);
|
||||
const isFocused = ref(false);
|
||||
const localReferences = ref<ReferenceChip[]>([...props.references]);
|
||||
|
|
@ -614,6 +618,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
resolvedPlaceholder,
|
||||
editableDiv,
|
||||
isFocused,
|
||||
handleInput,
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
class="size-5!"
|
||||
/>
|
||||
</Transition>
|
||||
<OTooltip side="top" align="center" :content="tooltipText" />
|
||||
<OTooltip side="top" align="center" :content="raw(tooltipText)" />
|
||||
</OButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, watch, onMounted, computed, defineComponent } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
|
|
@ -112,6 +112,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
store,
|
||||
darkMode,
|
||||
tooltipText,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ import { ref, computed, onMounted, watch } from "vue";
|
|||
import { useStore } from "vuex";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18nTyped();
|
||||
|
|
@ -149,7 +149,7 @@ interface WebinarData {
|
|||
id: number;
|
||||
documentId: string;
|
||||
tag: string;
|
||||
title: string;
|
||||
title: I18nText;
|
||||
date: string;
|
||||
primaryButton: PrimaryButton;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted, watch } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
getUUID,
|
||||
|
|
@ -484,11 +484,11 @@ const formData = ref(defaultActionScript);
|
|||
|
||||
const actionTypes = [
|
||||
{
|
||||
label: "Scheduled",
|
||||
label: t("home.scheduledAlert"),
|
||||
value: "scheduled",
|
||||
},
|
||||
{
|
||||
label: "Real Time",
|
||||
label: t("actions.realTime"),
|
||||
value: "service",
|
||||
},
|
||||
];
|
||||
|
|
@ -502,11 +502,11 @@ const dialog = ref({
|
|||
|
||||
const frequencyTabs = [
|
||||
{
|
||||
label: "Cron Job",
|
||||
label: t("actions.cronJob"),
|
||||
value: "repeat",
|
||||
},
|
||||
{
|
||||
label: "Once",
|
||||
label: t("reports.frequencyOnce"),
|
||||
value: "once",
|
||||
},
|
||||
];
|
||||
|
|
@ -980,7 +980,7 @@ const handleActionScript = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const filteredServiceAccounts: Ref<{ label: string; value: string }[]> = ref([]);
|
||||
const filteredServiceAccounts: Ref<{ label: I18nText; value: string }[]> = ref([]);
|
||||
const isFetchingServiceAccounts = ref(false);
|
||||
|
||||
const serviceAccountsOptions: any[] = [];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nKey } from "@/types/i18n";
|
||||
|
||||
/**
|
||||
* Builds a time-of-day greeting using the user's local timezone.
|
||||
|
|
@ -27,7 +27,10 @@ export function useGreeting(email: () => string | undefined) {
|
|||
});
|
||||
|
||||
const greeting = computed(() => {
|
||||
const key = `aiAssistant.greeting.${period.value}`;
|
||||
// Annotated so the built key is checked against I18nKey: `period` is a
|
||||
// literal union, so TS expands this to the four real keys and a rename or
|
||||
// deletion of any of them fails the build instead of rendering the path.
|
||||
const key: I18nKey = `aiAssistant.greeting.${period.value}`;
|
||||
const phrase = t(key);
|
||||
return displayName.value ? `${phrase}, ${displayName.value}` : phrase;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<div class="o2-input mb-4">
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -- example URL format, not translatable content -->
|
||||
<OFormInput
|
||||
placeholder="https://api.example.com/mcp/"
|
||||
:placeholder="raw('https://api.example.com/mcp/')"
|
||||
data-test="ai-toolset-mcp-url"
|
||||
name="mcp.url"
|
||||
:label="t('aiToolset.mcpUrl')"
|
||||
|
|
@ -154,7 +154,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<div class="o2-input mb-4">
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -- example CLI command name, not translatable content -->
|
||||
<OFormInput
|
||||
placeholder="kubectl"
|
||||
:placeholder="raw('kubectl')"
|
||||
data-test="ai-toolset-cli-command"
|
||||
name="cli.command"
|
||||
:label="t('aiToolset.cliCommand')"
|
||||
|
|
@ -166,7 +166,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<div class="o2-input mb-4">
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -- example CLI subcommand names, not translatable content -->
|
||||
<OFormInput
|
||||
placeholder="get, describe, logs"
|
||||
:placeholder="raw('get, describe, logs')"
|
||||
name="cli.allowed_subcommands_raw"
|
||||
:label="t('aiToolset.allowedSubcommands')"
|
||||
:helpText="t('aiToolset.subcommandsHint')"
|
||||
|
|
@ -246,7 +246,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OFormInput
|
||||
:name="`cli.credFiles[${idx}].key`"
|
||||
:label="t('aiToolset.credEnvVar')"
|
||||
helpText="e.g. KUBECONFIG"
|
||||
:helpText="raw('e.g. KUBECONFIG')"
|
||||
class="o2-input w-48"
|
||||
/>
|
||||
<OButton
|
||||
|
|
@ -329,7 +329,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
import { defineAsyncComponent, defineComponent, ref, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import aiToolsetsService from "@/services/ai_toolsets";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
|
@ -378,9 +378,9 @@ export default defineComponent({
|
|||
const isEditing = ref(false);
|
||||
|
||||
const kindOptions = [
|
||||
{ label: "MCP Server", value: "mcp" },
|
||||
{ label: "CLI Tool", value: "cli" },
|
||||
{ label: "Skill", value: "skill" },
|
||||
{ label: t("aiToolset.kindMcp"), value: "mcp" },
|
||||
{ label: t("aiToolset.kindCli"), value: "cli" },
|
||||
{ label: t("aiToolset.kindSkill"), value: "skill" },
|
||||
];
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -445,7 +445,7 @@ export default defineComponent({
|
|||
try {
|
||||
if (isEditing.value && editingId.value) {
|
||||
await aiToolsetsService.update(org, editingId.value, {
|
||||
description: value.description || undefined,
|
||||
description: value.description ? raw(value.description) : undefined,
|
||||
data,
|
||||
});
|
||||
toast({
|
||||
|
|
@ -456,7 +456,7 @@ export default defineComponent({
|
|||
await aiToolsetsService.create(org, {
|
||||
name: value.name,
|
||||
kind: value.kind as ToolsetKind,
|
||||
description: value.description || undefined,
|
||||
description: value.description ? raw(value.description) : undefined,
|
||||
data,
|
||||
});
|
||||
toast({
|
||||
|
|
@ -654,6 +654,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
// Exposed for the theme-aware footer background (`store.state.theme`).
|
||||
store,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</span>
|
||||
<span v-else class="truncate" :title="anomalyConfig.name">
|
||||
{{ anomalyConfig.name }}
|
||||
<OTooltip v-if="anomalyConfig.name?.length > 24" :content="anomalyConfig.name" />
|
||||
<OTooltip
|
||||
v-if="anomalyConfig.name?.length > 24"
|
||||
:content="raw(anomalyConfig.name)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -525,6 +528,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { raw } from "@/types/i18n";
|
||||
import { defineComponent, computed, watch, provide } from "vue";
|
||||
import type { SelectOption } from "@/lib/forms/Select/OSelect.types";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
@ -691,6 +695,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
...alertForm,
|
||||
isAnomalyDetectionEnabled,
|
||||
alertTypeOptions,
|
||||
|
|
|
|||
|
|
@ -440,7 +440,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed, onBeforeMount, onActivated, watch } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import destinationService from "@/services/alert_destination";
|
||||
import { useStore } from "vuex";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
@ -519,7 +519,7 @@ const apiHeaders = form.useStore(
|
|||
const isUpdatingDestination = ref(false);
|
||||
const isLoadingActions = ref(false);
|
||||
const router = useRouter();
|
||||
const actionOptions = ref<{ value: string; label: string; type: string }[]>([]);
|
||||
const actionOptions = ref<{ value: string; label: I18nText; type: string }[]>([]);
|
||||
|
||||
const { getAllActions } = useActions();
|
||||
|
||||
|
|
@ -895,17 +895,17 @@ const prebuiltTemplateOptions = computed(() => {
|
|||
return template.type !== "email";
|
||||
});
|
||||
|
||||
const options: { label: string; value: string }[] = [];
|
||||
const options: { label: I18nText; value: string }[] = [];
|
||||
|
||||
if (defaultPrebuiltTemplateName.value) {
|
||||
const defaultLabel = t("alert_destinations.templateDefaultOption", {
|
||||
name: defaultPrebuiltTemplateName.value,
|
||||
});
|
||||
options.push({ label: defaultLabel, value: defaultPrebuiltTemplateName.value });
|
||||
options.push({ label: raw(defaultLabel), value: defaultPrebuiltTemplateName.value });
|
||||
}
|
||||
|
||||
matching.forEach((template) => {
|
||||
options.push({ label: template.name, value: template.name });
|
||||
options.push({ label: raw(template.name), value: template.name });
|
||||
});
|
||||
|
||||
return options;
|
||||
|
|
|
|||
|
|
@ -433,13 +433,15 @@ describe("AlertHistory.vue", () => {
|
|||
expect(r.variant, s).toBe("success-soft");
|
||||
expect(r.icon, s).toBe("check-circle-outline");
|
||||
}
|
||||
expect(resolveBadge("alertState", "ok").label).toBe("Ok");
|
||||
// "Ok" moved from a hardcoded `label` to a translatable `labelKey`,
|
||||
// which OTag resolves via t() (label precedence: prop → labelKey → label).
|
||||
expect(resolveBadge("alertState", "ok").labelKey).toBe("components.badge.alertState.ok");
|
||||
});
|
||||
|
||||
it("condition_not_satisfied → green 'Ok' (matches the histogram count)", () => {
|
||||
const r = resolveBadge("alertState", "condition_not_satisfied");
|
||||
expect(r.variant).toBe("success-soft");
|
||||
expect(r.label).toBe("Ok");
|
||||
expect(r.labelKey).toBe("components.badge.alertState.ok");
|
||||
expect(r.icon).toBe("check-circle-outline");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
unit="us"
|
||||
mode="absolute"
|
||||
:timezone="store.state.timezone"
|
||||
empty-label="—"
|
||||
:empty-label="raw('—')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
unit="us"
|
||||
mode="absolute"
|
||||
:timezone="store.state.timezone"
|
||||
empty-label="—"
|
||||
:empty-label="raw('—')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
unit="us"
|
||||
mode="absolute"
|
||||
:timezone="store.state.timezone"
|
||||
empty-label="—"
|
||||
:empty-label="raw('—')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -449,7 +449,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
import { ref, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { formatDate } from "@/utils/date";
|
||||
import DateTime from "@/components/DateTime.vue";
|
||||
import OTable from "@/lib/core/Table/OTable.vue";
|
||||
|
|
|
|||
|
|
@ -135,21 +135,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
history: Array<{ status: string; timestamp: number }>;
|
||||
// Legend/tooltip wording. Defaults are alert-centric ("Firing"/"Ok");
|
||||
// workflows pass "Failed"/"Success".
|
||||
firingLabel?: string;
|
||||
okLabel?: string;
|
||||
}>(),
|
||||
{ firingLabel: "Firing", okLabel: "Ok" },
|
||||
);
|
||||
const { firingLabel, okLabel } = props;
|
||||
const props = defineProps<{
|
||||
history: Array<{ status: string; timestamp: number }>;
|
||||
// Legend/tooltip wording. Defaults are alert-centric ("Firing"/"Ok");
|
||||
// workflows pass "Failed"/"Success".
|
||||
firingLabel?: I18nText;
|
||||
okLabel?: I18nText;
|
||||
}>();
|
||||
|
||||
// Resolved here rather than as `withDefaults` defaults: a default is evaluated
|
||||
// once at module scope, which would freeze the wording in whatever locale
|
||||
// happened to be active when the module first loaded.
|
||||
const firingLabel = computed(() => props.firingLabel ?? t("alerts.historyTimeline.firing"));
|
||||
const okLabel = computed(() => props.okLabel ?? t("alerts.historyTimeline.ok"));
|
||||
|
||||
const hoveredIndex = ref<number | null>(null);
|
||||
|
||||
|
|
@ -167,10 +169,10 @@ function isOk(s: string) {
|
|||
|
||||
function normalizeStatus(s: string): string {
|
||||
const v = s?.toLowerCase();
|
||||
if (isFiring(v)) return firingLabel;
|
||||
if (isOk(v)) return okLabel;
|
||||
if (v === "skipped") return "Skipped";
|
||||
return s?.replace(/_/g, " ") ?? "Unknown";
|
||||
if (isFiring(v)) return firingLabel.value;
|
||||
if (isOk(v)) return okLabel.value;
|
||||
if (v === "skipped") return t("alerts.historyTimeline.skipped");
|
||||
return s?.replace(/_/g, " ") ?? t("alerts.historyTimeline.unknown");
|
||||
}
|
||||
|
||||
function blockColor(status: string): string {
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
unit="iso"
|
||||
mode="absolute"
|
||||
:timezone="store.state.timezone"
|
||||
empty-label="—"
|
||||
:empty-label="raw('—')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -709,7 +709,7 @@ import { useRouter } from "vue-router";
|
|||
import useStreams from "@/composables/useStreams";
|
||||
|
||||
import { convertUnixToDateFormat as convertUnixToFormat } from "@/utils/date";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { debounce } from "lodash-es";
|
||||
import alertsService from "@/services/alerts";
|
||||
import destinationService from "@/services/alert_destination";
|
||||
|
|
@ -2835,6 +2835,7 @@ export default defineComponent({
|
|||
]);
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
store,
|
||||
router,
|
||||
|
|
|
|||
|
|
@ -82,13 +82,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OSelect from "@/lib/forms/Select/OSelect.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
label: I18nText;
|
||||
value: string;
|
||||
}
|
||||
// Option lists may be plain strings (destinations = `getFormattedDestinations`
|
||||
|
|
@ -152,12 +152,12 @@ watch(propsCombined, (next) => {
|
|||
// Normalize a raw option (string or {label,value}) to a { name, label } pair.
|
||||
// Returns null for anything unusable (see the isFilled note above) so the caller
|
||||
// can drop it rather than render a `dest:undefined` row or throw.
|
||||
const norm = (o: RawOption): { name: string; label: string } | null => {
|
||||
const norm = (o: RawOption): { name: string; label: I18nText } | null => {
|
||||
if (!isFilled(o)) return null;
|
||||
if (typeof o === "string") return { name: o, label: o };
|
||||
if (typeof o === "string") return { name: o, label: raw(o) };
|
||||
if (!isFilled((o as Option).value)) return null;
|
||||
const value = String((o as Option).value);
|
||||
return { name: value, label: (o as Option).label ?? value };
|
||||
return { name: value, label: (o as Option).label ?? raw(value) };
|
||||
};
|
||||
|
||||
const toTagged = (list: RawOption[] | undefined, tag: string) =>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="custom-confirm-dialog"
|
||||
v-model:open="isVisible"
|
||||
size="sm"
|
||||
:title="title"
|
||||
:title="resolvedTitle"
|
||||
persistent
|
||||
:show-close="false"
|
||||
:secondary-button-label="t('common.cancel')"
|
||||
|
|
@ -34,9 +34,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch } from "vue";
|
||||
import { computed, defineComponent, ref, watch, type PropType } from "vue";
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CustomConfirmDialog",
|
||||
|
|
@ -47,12 +47,14 @@ export default defineComponent({
|
|||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "Confirm Action",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
// Resolved in setup, not here: a literal default would ship untranslated,
|
||||
// and t() at module scope would freeze the copy at page-load locale.
|
||||
default: undefined,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "confirm", "cancel"],
|
||||
|
|
@ -76,12 +78,16 @@ export default defineComponent({
|
|||
emit("cancel");
|
||||
};
|
||||
|
||||
const resolvedTitle = computed(() => props.title ?? t("common.confirmAction"));
|
||||
|
||||
const onConfirm = () => {
|
||||
isVisible.value = false;
|
||||
emit("confirm");
|
||||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
resolvedTitle,
|
||||
isVisible,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
<ODialog
|
||||
v-model:open="isOpen"
|
||||
size="md"
|
||||
:title="`${t('alerts.destinationPreview')} - ${getDestinationTypeName(type)}`"
|
||||
:title="raw(`${t('alerts.destinationPreview')} - ${getDestinationTypeName(type)}`)"
|
||||
data-test="destination-preview-dialog"
|
||||
>
|
||||
<div data-test="destination-preview-card" class="w-full">
|
||||
|
|
@ -398,7 +398,7 @@ limitations under the License.
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
|
|
@ -447,8 +447,8 @@ const getDestinationTypeName = (type: string): string => {
|
|||
// Copy template to clipboard
|
||||
const copyTemplate = () => {
|
||||
copyToClipboard(props.templateContent, t, {
|
||||
successMessage: "Template copied to clipboard",
|
||||
errorMessage: "Failed to copy template",
|
||||
successMessage: t("alerts.previewCopyTemplateSuccess"),
|
||||
errorMessage: t("alerts.previewCopyTemplateError"),
|
||||
timeout: 2000,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:placeholder="t('alerts.column')"
|
||||
:creatable="props.enableNewValueMode"
|
||||
:error="!!fieldErrors[`${field.uuid}-column`]"
|
||||
:error-message="fieldErrors[`${field.uuid}-column`] || ''"
|
||||
:error-message="raw(fieldErrors[`${field.uuid}-column`] || '')"
|
||||
data-test="alert-conditions-select-column"
|
||||
@create="
|
||||
(val: string) => {
|
||||
|
|
@ -162,7 +162,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:options="triggerOperators"
|
||||
class="min-w-30 py-2"
|
||||
:error="!!fieldErrors[`${field.uuid}-operator`]"
|
||||
:error-message="fieldErrors[`${field.uuid}-operator`] || ''"
|
||||
:error-message="raw(fieldErrors[`${field.uuid}-operator`] || '')"
|
||||
data-test="alert-conditions-operator-select"
|
||||
@update:model-value="
|
||||
(v: any) => {
|
||||
|
|
@ -180,7 +180,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:placeholder="t('common.value')"
|
||||
class="min-w-37.5 py-2"
|
||||
:error="!!fieldErrors[`${field.uuid}-value`]"
|
||||
:error-message="fieldErrors[`${field.uuid}-value`] || ''"
|
||||
:error-message="raw(fieldErrors[`${field.uuid}-value`] || '')"
|
||||
data-test="alert-conditions-value-input"
|
||||
@update:model-value="
|
||||
(v: any) => {
|
||||
|
|
@ -224,7 +224,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed, reactive, inject } from "vue";
|
||||
import type { PropType, Ref } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OInput from "@/lib/forms/Input/OInput.vue";
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
required: true,
|
||||
},
|
||||
depth: {
|
||||
|
|
@ -156,8 +156,8 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
import { ref, computed, watch, inject } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { ref, computed, watch, inject, type PropType } from "vue";
|
||||
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OInput
|
||||
data-test="alert-import-name-input"
|
||||
:model-value="userSelectedAlertName[index] || ''"
|
||||
:label="t('alerts.name') + ' *'"
|
||||
:label="raw(t('alerts.name') + ' *')"
|
||||
:error="!userSelectedAlertName[index]?.toString().trim()"
|
||||
:error-message="t('alerts.validation.fieldRequired')"
|
||||
@update:model-value="
|
||||
|
|
@ -139,7 +139,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="alert-import-stream-name-input"
|
||||
:model-value="userSelectedStreamName[index] || ''"
|
||||
:options="streamList"
|
||||
:label="t('alerts.stream_name') + ' *'"
|
||||
:label="raw(t('alerts.stream_name') + ' *')"
|
||||
searchable
|
||||
:error="!userSelectedStreamName[index]"
|
||||
:error-message="t('alerts.validation.fieldRequired')"
|
||||
|
|
@ -192,7 +192,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="alert-import-stream-type-input"
|
||||
:model-value="userSelectedStreamType[index] || ''"
|
||||
:options="streamTypes"
|
||||
:label="t('alerts.streamType') + ' *'"
|
||||
:label="raw(t('alerts.streamType') + ' *')"
|
||||
class="w-75!"
|
||||
:error="!userSelectedStreamType[index]"
|
||||
:error-message="t('alerts.validation.fieldRequired')"
|
||||
|
|
@ -295,7 +295,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, computed, watch } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import alertsService from "../../services/alerts";
|
||||
|
|
@ -347,10 +347,10 @@ export default defineComponent({
|
|||
setup(props, { emit }) {
|
||||
type ErrorMessage = {
|
||||
field: string;
|
||||
message: string;
|
||||
message: I18nText;
|
||||
};
|
||||
type alertCreator = {
|
||||
message: string;
|
||||
message: I18nText;
|
||||
success: boolean;
|
||||
}[];
|
||||
|
||||
|
|
@ -400,7 +400,7 @@ export default defineComponent({
|
|||
const organizationDataList = computed(() => {
|
||||
return store.state.organizations.map((org: any) => {
|
||||
return {
|
||||
label: org.identifier,
|
||||
label: raw(org.identifier),
|
||||
value: org.identifier,
|
||||
disabled:
|
||||
!org.identifier || org.identifier !== store.state.selectedOrganization.identifier,
|
||||
|
|
@ -568,13 +568,17 @@ export default defineComponent({
|
|||
};
|
||||
await anomalyDetectionService.create(org, payload);
|
||||
alertCreators.value.push({
|
||||
message: `Anomaly Detection - ${index}: "${jsonObj.name}" imported successfully`,
|
||||
message: t("alerts.import.anomalyImportSuccess", { index, name: jsonObj.name }),
|
||||
success: true,
|
||||
});
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
alertCreators.value.push({
|
||||
message: `Anomaly Detection - ${index}: "${jsonObj.name}" import failed — ${e?.response?.data?.message || "Unknown Error"}`,
|
||||
message: t("alerts.import.anomalyImportFailed", {
|
||||
index,
|
||||
name: jsonObj.name,
|
||||
reason: e?.response?.data?.message || t("alerts.import.unknownError"),
|
||||
}),
|
||||
success: false,
|
||||
});
|
||||
return false;
|
||||
|
|
@ -608,12 +612,12 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const validateAlertInputs = async (input: any, index: number) => {
|
||||
let alertErrors: (string | { message: string; field: string })[] = [];
|
||||
let alertErrors: (string | ErrorMessage)[] = [];
|
||||
|
||||
// 1. Validate 'name' field
|
||||
if (!input.name || typeof input.name !== "string" || input.name.trim() === "") {
|
||||
alertErrors.push({
|
||||
message: `Alert - ${index}: Name is mandatory and should be a valid string.`,
|
||||
message: t("alerts.import.nameRequired", { index }),
|
||||
field: "alert_name",
|
||||
});
|
||||
}
|
||||
|
|
@ -626,7 +630,10 @@ export default defineComponent({
|
|||
input.org_id != store.state.selectedOrganization.identifier
|
||||
) {
|
||||
alertErrors.push({
|
||||
message: `Alert - ${index}: Organization Id is mandatory, should exist in organization list and should be equal to ${store.state.selectedOrganization.identifier}.`,
|
||||
message: t("alerts.import.orgIdInvalid", {
|
||||
index,
|
||||
orgId: store.state.selectedOrganization.identifier,
|
||||
}),
|
||||
field: "org_id",
|
||||
});
|
||||
}
|
||||
|
|
@ -635,7 +642,7 @@ export default defineComponent({
|
|||
const validStreamTypes = ["logs", "metrics", "traces"];
|
||||
if (!input.stream_type || !validStreamTypes.includes(input.stream_type)) {
|
||||
alertErrors.push({
|
||||
message: `Alert - ${index}: Stream Type is mandatory and should be one of: 'logs', 'metrics', 'traces'.`,
|
||||
message: t("alerts.import.streamTypeInvalid", { index }),
|
||||
field: "stream_type",
|
||||
});
|
||||
}
|
||||
|
|
@ -658,7 +665,7 @@ export default defineComponent({
|
|||
!streamList.value.includes(input.stream_name)
|
||||
) {
|
||||
alertErrors.push({
|
||||
message: `Alert - ${index}: Stream Name is mandatory, should exist in the stream list and should be a valid string.`,
|
||||
message: t("alerts.import.streamNameInvalid", { index }),
|
||||
field: "stream_name",
|
||||
});
|
||||
}
|
||||
|
|
@ -895,17 +902,17 @@ export default defineComponent({
|
|||
input.destinations.length === 0
|
||||
) {
|
||||
alertErrors.push({
|
||||
message: `Alert - ${index}: Destinations are required and should be an array.`,
|
||||
message: t("alerts.import.destinationsRequired", { index }),
|
||||
field: "destination_name",
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof input.enabled !== "boolean") {
|
||||
alertErrors.push(`Alert - ${index}: Enabled should be Boolean.`);
|
||||
alertErrors.push(t("alerts.import.enabledBoolean", { index }));
|
||||
}
|
||||
|
||||
if (input.tz_offset && (typeof input.tz_offset !== "number" || input.tz_offset < 0)) {
|
||||
alertErrors.push(`Alert - ${index}: Timezone offset should be a number.`);
|
||||
alertErrors.push(t("alerts.import.tzOffsetNumber", { index }));
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -914,7 +921,7 @@ export default defineComponent({
|
|||
input.trigger_condition.timezone === ""
|
||||
) {
|
||||
alertErrors.push({
|
||||
message: `Alert - ${index}: Timezone is required when frequency type is 'cron'.`,
|
||||
message: t("alerts.import.timezoneRequiredForCron", { index }),
|
||||
field: "timezone",
|
||||
});
|
||||
}
|
||||
|
|
@ -922,7 +929,7 @@ export default defineComponent({
|
|||
input.destinations.forEach((destination: any) => {
|
||||
if (!checkDestinationInList(props.destinations, destination)) {
|
||||
alertErrors.push({
|
||||
message: `Alert - ${index}: "${destination}" destination does not exist`,
|
||||
message: t("alerts.import.destinationNotExist", { index, destination }),
|
||||
field: "destination_name",
|
||||
});
|
||||
}
|
||||
|
|
@ -1006,7 +1013,7 @@ export default defineComponent({
|
|||
|
||||
// Success
|
||||
alertCreators.value.push({
|
||||
message: `Alert - ${index}: "${input.name}" created successfully \nNote: please remove the created alert object ${input.name} from the json file`,
|
||||
message: t("alerts.import.createSuccess", { index, name: input.name }),
|
||||
success: true,
|
||||
});
|
||||
// Emit update after each successful creation
|
||||
|
|
@ -1016,7 +1023,11 @@ export default defineComponent({
|
|||
} catch (error: any) {
|
||||
// Failure
|
||||
alertCreators.value.push({
|
||||
message: `Alert - ${index}: "${input.name}" creation failed --> \n Reason: ${error?.response?.data?.message || "Unknown Error"}`,
|
||||
message: t("alerts.import.createFailed", {
|
||||
index,
|
||||
name: input.name,
|
||||
reason: error?.response?.data?.message || t("alerts.import.unknownError"),
|
||||
}),
|
||||
success: false,
|
||||
});
|
||||
return false;
|
||||
|
|
@ -1134,6 +1145,7 @@ export default defineComponent({
|
|||
|
||||
return {
|
||||
t,
|
||||
raw,
|
||||
importJson,
|
||||
router,
|
||||
baseImportRef,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
updateDestinationName(val, index);
|
||||
}
|
||||
"
|
||||
:label="t('alert_destinations.import.destinationName') + ' *'"
|
||||
:label="raw(t('alert_destinations.import.destinationName') + ' *')"
|
||||
class="showLabelOnTop"
|
||||
tabindex="0"
|
||||
/>
|
||||
|
|
@ -97,7 +97,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
updateDestinationUrl(val, index);
|
||||
}
|
||||
"
|
||||
:label="t('alert_destinations.import.destinationUrl') + ' *'"
|
||||
:label="raw(t('alert_destinations.import.destinationUrl') + ' *')"
|
||||
class="showLabelOnTop"
|
||||
tabindex="0"
|
||||
/>
|
||||
|
|
@ -121,7 +121,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
}
|
||||
"
|
||||
:options="destinationTypes"
|
||||
:label="t('alert_destinations.destination_type') + ' *'"
|
||||
:label="raw(t('alert_destinations.destination_type') + ' *')"
|
||||
class="showLabelOnTop no-case py-2"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -144,7 +144,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
}
|
||||
"
|
||||
:options="destinationMethods"
|
||||
:label="t('alert_destinations.import.destinationMethod') + ' *'"
|
||||
:label="raw(t('alert_destinations.import.destinationMethod') + ' *')"
|
||||
class="showLabelOnTop no-case py-2"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -170,10 +170,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
}
|
||||
"
|
||||
:options="filteredTemplates"
|
||||
:label="t('alert_destinations.import.templates') + ' *'"
|
||||
:label="raw(t('alert_destinations.import.templates') + ' *')"
|
||||
class="showLabelOnTop no-case py-2"
|
||||
:error="!!templateErrors[index]"
|
||||
:error-message="templateErrors[index]"
|
||||
:error-message="raw(templateErrors[index])"
|
||||
@search="filterTemplates"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -197,7 +197,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
updateDestinationEmails(val, index);
|
||||
}
|
||||
"
|
||||
:label="t('alert_destinations.import.emails') + ' *'"
|
||||
:label="raw(t('alert_destinations.import.emails') + ' *')"
|
||||
class="showLabelOnTop"
|
||||
tabindex="0"
|
||||
/>
|
||||
|
|
@ -224,12 +224,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
}
|
||||
"
|
||||
:options="filteredActions"
|
||||
:label="t('alert_destinations.import.actions') + ' *'"
|
||||
:label="raw(t('alert_destinations.import.actions') + ' *')"
|
||||
labelKey="label"
|
||||
valueKey="value"
|
||||
class="showLabelOnTop no-case w-75! py-2"
|
||||
:error="!!actionErrors[index]"
|
||||
:error-message="actionErrors[index]"
|
||||
:error-message="raw(actionErrors[index])"
|
||||
@search="filterActions"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -297,7 +297,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, reactive, onMounted } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import destinationService from "@/services/alert_destination";
|
||||
|
|
@ -331,10 +331,10 @@ export default defineComponent({
|
|||
setup(props, { emit }) {
|
||||
type ErrorMessage = {
|
||||
field: string;
|
||||
message: string;
|
||||
message: I18nText;
|
||||
};
|
||||
type destinationCreator = {
|
||||
message: string;
|
||||
message: I18nText;
|
||||
success: boolean;
|
||||
}[];
|
||||
type destinationErrors = (ErrorMessage | string)[][];
|
||||
|
|
@ -402,7 +402,7 @@ export default defineComponent({
|
|||
userSelectedActionOptions.value = actionsData.list
|
||||
.filter((action: any) => action.execution_details_type === "service")
|
||||
.map((action: any) => ({
|
||||
label: action.name,
|
||||
label: raw(action.name),
|
||||
value: action.id,
|
||||
}));
|
||||
filteredActions.value = userSelectedActionOptions.value;
|
||||
|
|
@ -622,7 +622,7 @@ export default defineComponent({
|
|||
value ? "" : t("alerts.validation.fieldRequired");
|
||||
|
||||
const validateDestinationInputs = async (input: any, index: number) => {
|
||||
let destinationErrors: (string | { message: string; field: string })[] = [];
|
||||
let destinationErrors: (string | ErrorMessage)[] = [];
|
||||
|
||||
// Validate name
|
||||
if (!input.name || typeof input.name !== "string" || input.name.trim() === "") {
|
||||
|
|
@ -831,6 +831,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
importJson,
|
||||
router,
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="import-semantic-groups-group-dialog"
|
||||
v-model:open="showGroupDialog"
|
||||
size="md"
|
||||
:title="selectedGroup?.display"
|
||||
:title="raw(selectedGroup?.display)"
|
||||
:sub-title="t('common.idPrefix', { id: selectedGroup?.id })"
|
||||
:primary-button-label="t('common.close')"
|
||||
@click:primary="showGroupDialog = false"
|
||||
|
|
@ -282,7 +282,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="import-semantic-groups-modification-dialog"
|
||||
v-model:open="showModificationDialog"
|
||||
size="lg"
|
||||
:title="selectedModification?.proposed.display"
|
||||
:title="raw(selectedModification?.proposed.display)"
|
||||
:sub-title="t('correlation.importSemanticGroups.compareChanges')"
|
||||
:primary-button-label="t('common.close')"
|
||||
@click:primary="showModificationDialog = false"
|
||||
|
|
@ -355,7 +355,7 @@ import BaseImport from "@/components/common/BaseImport.vue";
|
|||
import alertsService from "@/services/alerts";
|
||||
import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
|
||||
import { toast } from "@/lib/feedback/Toast/useToast";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
|
||||
interface SemanticGroup {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="import-semantic-groups-drawer-group-dialog"
|
||||
v-model:open="showGroupDialog"
|
||||
size="md"
|
||||
:title="selectedGroup?.display"
|
||||
:title="raw(selectedGroup?.display)"
|
||||
:sub-title="t('common.idPrefix', { id: selectedGroup?.id })"
|
||||
:primary-button-label="t('common.close')"
|
||||
@click:primary="showGroupDialog = false"
|
||||
|
|
@ -254,7 +254,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="import-semantic-groups-drawer-modification-dialog"
|
||||
v-model:open="showModificationDialog"
|
||||
size="lg"
|
||||
:title="selectedModification?.proposed.display"
|
||||
:title="raw(selectedModification?.proposed.display)"
|
||||
:sub-title="t('correlation.compareChanges')"
|
||||
:primary-button-label="t('common.close')"
|
||||
@click:primary="showModificationDialog = false"
|
||||
|
|
@ -320,7 +320,7 @@ import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
|
|||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OCollapsible from "@/lib/core/Collapsible/OCollapsible.vue";
|
||||
import { toast } from "@/lib/feedback/Toast/useToast";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import templateService from "@/services/alert_templates";
|
||||
|
|
@ -218,10 +218,10 @@ export default defineComponent({
|
|||
setup(props, { emit }) {
|
||||
type ErrorMessage = {
|
||||
field: string;
|
||||
message: string;
|
||||
message: I18nText;
|
||||
};
|
||||
type templateCreator = {
|
||||
message: string;
|
||||
message: I18nText;
|
||||
success: boolean;
|
||||
}[];
|
||||
type templateErrors = (ErrorMessage | string)[][];
|
||||
|
|
@ -369,17 +369,17 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const validateTemplateInputs = async (input: any, index: number) => {
|
||||
let templateErrors: (string | { message: string; field: string })[] = [];
|
||||
let templateErrors: (string | ErrorMessage)[] = [];
|
||||
|
||||
// Validate name using the updated props.templates
|
||||
if (!input.name || typeof input.name !== "string" || input.name.trim() === "") {
|
||||
templateErrors.push({
|
||||
message: `Template - ${index}: The "name" field is required and should be a valid string.`,
|
||||
message: t("alert_templates.import.nameRequired", { index }),
|
||||
field: "template_name",
|
||||
});
|
||||
} else if (props.templates.some((template: any) => template.name === input.name)) {
|
||||
templateErrors.push({
|
||||
message: `Template - ${index}: "${input.name}" already exists`,
|
||||
message: t("alert_templates.import.nameExists", { index, name: input.name }),
|
||||
field: "template_name",
|
||||
});
|
||||
}
|
||||
|
|
@ -387,7 +387,7 @@ export default defineComponent({
|
|||
// Validate type
|
||||
if (!input.type || (input.type !== "email" && input.type !== "http")) {
|
||||
templateErrors.push({
|
||||
message: `Template - ${index}: The "type" field must be either "email" or "http"`,
|
||||
message: t("alert_templates.import.typeInvalid", { index }),
|
||||
field: "type",
|
||||
});
|
||||
}
|
||||
|
|
@ -395,14 +395,14 @@ export default defineComponent({
|
|||
// Validate body
|
||||
if (!input.body || typeof input.body !== "string" || input.body.trim() === "") {
|
||||
templateErrors.push({
|
||||
message: `Template - ${index}: The "body" field is required and should be a valid JSON string.`,
|
||||
message: t("alert_templates.import.bodyRequired", { index }),
|
||||
field: "body",
|
||||
});
|
||||
} else {
|
||||
const result = validateTemplateBody(input.body);
|
||||
if (!result.valid) {
|
||||
templateErrors.push({
|
||||
message: `Template - ${index}: The "body" field should contain valid JSON. Placeholders like {value} for numbers and "{name}" for strings are supported.`,
|
||||
message: t("alert_templates.import.bodyInvalidJson", { index }),
|
||||
field: "body",
|
||||
});
|
||||
}
|
||||
|
|
@ -412,7 +412,7 @@ export default defineComponent({
|
|||
if (input.type === "email") {
|
||||
if (!input.title || typeof input.title !== "string" || input.title.trim() === "") {
|
||||
templateErrors.push({
|
||||
message: `Template - ${index}: The "title" field is required for email type templates.`,
|
||||
message: t("alert_templates.import.titleRequiredEmail", { index }),
|
||||
field: "title",
|
||||
});
|
||||
}
|
||||
|
|
@ -445,7 +445,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
tempalteCreators.value.push({
|
||||
message: `Template - ${index}: "${input.name}" created successfully \nNote: please remove the created alert object ${input.name} from the json file `,
|
||||
message: t("alert_templates.import.createSuccess", { index, name: input.name }),
|
||||
success: true,
|
||||
});
|
||||
|
||||
|
|
@ -455,7 +455,11 @@ export default defineComponent({
|
|||
return true;
|
||||
} catch (error: any) {
|
||||
tempalteCreators.value.push({
|
||||
message: `Template - ${index}: "${input.name}" creation failed --> \n Reason: ${error?.response?.data?.message || "Unknown Error"}`,
|
||||
message: t("alert_templates.import.createFailed", {
|
||||
index,
|
||||
name: input.name,
|
||||
reason: error?.response?.data?.message || t("alert_templates.import.unknownError"),
|
||||
}),
|
||||
success: false,
|
||||
});
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
type="correlationReason"
|
||||
:value="row.correlation_reason"
|
||||
/>
|
||||
<OTooltip :content="getReasonTooltip(row.correlation_reason)" side="top" />
|
||||
<OTooltip :content="raw(getReasonTooltip(row.correlation_reason))" side="top" />
|
||||
</span>
|
||||
</template>
|
||||
</OTable>
|
||||
|
|
@ -71,7 +71,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { formatToReadable } from "@/utils/date";
|
||||
import OTag from "@/lib/core/Badge/OTag.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
|
|
@ -168,6 +168,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
columns,
|
||||
formatTimestamp,
|
||||
|
|
|
|||
|
|
@ -1233,8 +1233,9 @@ describe("IncidentDetailDrawer.vue", () => {
|
|||
});
|
||||
|
||||
it("should have translation for fired times with parameter", () => {
|
||||
const translation = wrapper.vm.t("alerts.incidents.firedTimes", { count: 5 });
|
||||
expect(translation).toBe("Fired 5 time(s)");
|
||||
// Pipe plural: `count` in the named bag selects the branch AND fills {count}.
|
||||
expect(wrapper.vm.t("alerts.incidents.firedTimes", { count: 5 })).toBe("Fired 5 times");
|
||||
expect(wrapper.vm.t("alerts.incidents.firedTimes", { count: 1 })).toBe("Fired 1 time");
|
||||
});
|
||||
|
||||
it("should have translation for refresh correlated data", () => {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
{{ incidentDetails.title }}
|
||||
<OTooltip
|
||||
v-if="incidentDetails && (incidentDetails.title?.length ?? 0) > 35"
|
||||
:content="incidentDetails.title"
|
||||
:content="raw(incidentDetails.title)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -58,14 +58,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OTag type="incidentStatus" :value="incidentDetails.status" />
|
||||
<OTooltip
|
||||
:content="
|
||||
t('alerts.incidents.status') + ': ' + getStatusLabel(incidentDetails.status)
|
||||
raw(t('alerts.incidents.status') + ': ' + getStatusLabel(incidentDetails.status))
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span class="inline-flex cursor-default">
|
||||
<OTag type="severity" :value="incidentDetails.severity" />
|
||||
<OTooltip :content="t('alerts.incidents.severity') + ': ' + incidentDetails.severity" />
|
||||
<OTooltip
|
||||
:content="raw(t('alerts.incidents.severity') + ': ' + incidentDetails.severity)"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span class="inline-flex cursor-default">
|
||||
|
|
@ -759,7 +761,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
{{ index + 1 }}.
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<OTooltip v-if="alert.name.length > 30" :content="alert.name" />
|
||||
<OTooltip
|
||||
v-if="alert.name.length > 30"
|
||||
:content="raw(alert.name)"
|
||||
/>
|
||||
<span class="block truncate font-medium">
|
||||
{{
|
||||
alert.name.length > 30
|
||||
|
|
@ -1363,7 +1368,7 @@ import {
|
|||
onBeforeUnmount,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useRouter } from "vue-router";
|
||||
|
|
@ -1481,16 +1486,16 @@ export default defineComponent({
|
|||
|
||||
// Status and Severity options
|
||||
const statusOptions = [
|
||||
{ label: "Open", value: "open" },
|
||||
{ label: "Acknowledged", value: "acknowledged" },
|
||||
{ label: "Resolved", value: "resolved" },
|
||||
{ label: t("alerts.incidents.statusOpen"), value: "open" },
|
||||
{ label: t("alerts.incidents.statusAcknowledged"), value: "acknowledged" },
|
||||
{ label: t("alerts.incidents.statusResolved"), value: "resolved" },
|
||||
];
|
||||
|
||||
const severityOptions = [
|
||||
{ label: "P1 - Critical", value: "P1" },
|
||||
{ label: "P2 - High", value: "P2" },
|
||||
{ label: "P3 - Medium", value: "P3" },
|
||||
{ label: "P4 - Low", value: "P4" },
|
||||
{ label: t("alerts.incidents.severityP1"), value: "P1" },
|
||||
{ label: t("alerts.incidents.severityP2"), value: "P2" },
|
||||
{ label: t("alerts.incidents.severityP3"), value: "P3" },
|
||||
{ label: t("alerts.incidents.severityP4"), value: "P4" },
|
||||
];
|
||||
|
||||
// Table of Contents
|
||||
|
|
@ -2774,10 +2779,10 @@ export default defineComponent({
|
|||
});
|
||||
} else {
|
||||
const ok = await confirm({
|
||||
title: "Re-run AI Analysis?",
|
||||
message: "Severity has changed. Would you like AI to re-analyze this incident?",
|
||||
confirmLabel: "Re-run AI analysis",
|
||||
cancelLabel: "No thanks",
|
||||
title: t("alerts.incidents.rerunAnalysisTitle"),
|
||||
message: t("alerts.incidents.rerunAnalysisMessage"),
|
||||
confirmLabel: t("alerts.incidents.rerunAnalysisConfirmLabel"),
|
||||
cancelLabel: t("alerts.incidents.rerunAnalysisCancelLabel"),
|
||||
persistent: false,
|
||||
});
|
||||
if (ok) {
|
||||
|
|
@ -3355,6 +3360,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
store,
|
||||
loading,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OTag
|
||||
type="incidentStatus"
|
||||
:value="row.status"
|
||||
:label="getStatusLabel(row.status)"
|
||||
:label="raw(getStatusLabel(row.status))"
|
||||
size="sm"
|
||||
data-test="incident-status-badge"
|
||||
/>
|
||||
|
|
@ -183,7 +183,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
unit="us"
|
||||
mode="relative"
|
||||
:timezone="store.state.timezone"
|
||||
empty-label="—"
|
||||
:empty-label="raw('—')"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
|
|
@ -251,7 +251,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, shallowRef, computed, onMounted, watch, nextTick } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { formatToReadable } from "@/utils/date";
|
||||
|
|
@ -813,6 +813,7 @@ export default defineComponent({
|
|||
]);
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
loading,
|
||||
allIncidents,
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
side="bottom"
|
||||
align="start"
|
||||
:max-width="'24rem'"
|
||||
:content="getFailureTooltip(event)"
|
||||
:content="raw(getFailureTooltip(event))"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
|
|
@ -286,7 +286,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script lang="ts" setup>
|
||||
import { ref, onMounted, watch, nextTick } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { formatToDateOnly } from "@/utils/date";
|
||||
import incidentsService from "@/services/incidents";
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:data-test="'organizationdeduplication-fingerprint-' + group.id + '-checkbox'"
|
||||
:key="group.id"
|
||||
:value="group.id"
|
||||
:label="`${group.display} (${group.id})`"
|
||||
:label="raw(`${group.display} (${group.id})`)"
|
||||
/>
|
||||
</OFormCheckboxGroup>
|
||||
</div>
|
||||
|
|
@ -147,7 +147,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import alertsService from "@/services/alerts";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import { reactive } from "vue";
|
|||
import { onBeforeMount } from "vue";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import searchService from "@/services/search";
|
||||
import { b64EncodeUnicode, smartDecodeVrlFunction } from "@/utils/zincutils";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
|
@ -1013,7 +1013,7 @@ const refreshData = () => {
|
|||
];
|
||||
|
||||
let yAxis: Array<{
|
||||
label: string;
|
||||
label: I18nText;
|
||||
alias: string;
|
||||
column: string;
|
||||
color: string | null;
|
||||
|
|
@ -1080,7 +1080,7 @@ const refreshData = () => {
|
|||
// Configure y-axis for zo_sql_num (counts)
|
||||
yAxis = [
|
||||
{
|
||||
label: "count",
|
||||
label: raw("count"),
|
||||
alias: "zo_sql_num",
|
||||
column: "zo_sql_num",
|
||||
color: "#5960b2",
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
side="top"
|
||||
align="center"
|
||||
:max-width="'520px'"
|
||||
:content="localSqlQueryErrorMsg || sqlQueryErrorMsg"
|
||||
:content="raw(localSqlQueryErrorMsg || sqlQueryErrorMsg)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -318,7 +318,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:query="vrlFunctionContent"
|
||||
:hide-nl-toggle="false"
|
||||
:disable-ai="false"
|
||||
:disable-ai-reason="''"
|
||||
:disable-ai-reason="raw('')"
|
||||
:ai-placeholder="t('search.askAIFunctionPlaceholder')"
|
||||
:ai-tooltip="t('search.enterFunctionPrompt')"
|
||||
:debounce-time="300"
|
||||
|
|
@ -514,7 +514,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch, type PropType, onMounted, inject, type Ref } from "vue";
|
||||
import { type SqlErrorRange } from "@/utils/query/sqlDiagnostics";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import useTheme from "@/composables/useTheme";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:key="group.id"
|
||||
v-model="localFingerprintFields"
|
||||
:value="group.id"
|
||||
:label="group.display"
|
||||
:label="raw(group.display)"
|
||||
class="min-w-50"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
|
@ -139,7 +139,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, nextTick } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import SemanticGroupItem from "./SemanticGroupItem.vue";
|
||||
import ImportSemanticGroupsDrawer from "./ImportSemanticGroupsDrawer.vue";
|
||||
|
|
@ -248,7 +248,7 @@ const categoryOptions = computed(() => {
|
|||
return Array.from(groupsMap.entries())
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([category, count]) => ({
|
||||
label: category,
|
||||
label: raw(category),
|
||||
value: category,
|
||||
count: count,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, inject, type PropType, type Ref } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { getUUID } from "@/utils/zincutils";
|
||||
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
|
||||
|
|
@ -269,8 +269,8 @@ export default defineComponent({
|
|||
default: () => [],
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
rowTemplate: {
|
||||
type: String,
|
||||
|
|
@ -404,6 +404,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
store,
|
||||
variableRows,
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, onMounted, ref, type PropType } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import OFormInput from "@/lib/forms/Input/OFormInput.vue";
|
||||
|
|
@ -346,7 +346,7 @@ export default defineComponent({
|
|||
(config.isEnterprise === "true" || config.isCloud === "true") &&
|
||||
store.state.zoConfig?.workflows_enabled === true,
|
||||
);
|
||||
const workflowOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const workflowOptions = ref<{ label: I18nText; value: string }[]>([]);
|
||||
const fetchWorkflows = async () => {
|
||||
if (!workflowsEnabled.value) return;
|
||||
try {
|
||||
|
|
@ -355,7 +355,8 @@ export default defineComponent({
|
|||
);
|
||||
const list = Array.isArray(res.data) ? res.data : (res.data?.list ?? []);
|
||||
workflowOptions.value = list.map((wf: any) => ({
|
||||
label: wf.name,
|
||||
// Workflow names come from the server — never translated copy.
|
||||
label: raw(wf.name),
|
||||
value: wf.id,
|
||||
}));
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
{{ t("alerts.compareWithPast.addComparisonWindow") }}
|
||||
<OTooltip
|
||||
v-if="isComparisonDisabled"
|
||||
:content="comparisonDisabledTooltip"
|
||||
:content="raw(comparisonDisabledTooltip)"
|
||||
side="top"
|
||||
align="center"
|
||||
:sideOffset="8"
|
||||
|
|
@ -229,7 +229,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, computed, inject, type PropType } from "vue";
|
||||
import { useI18nTyped, type I18nKey } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nKey } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { getUUID } from "@/utils/zincutils";
|
||||
import CustomDateTimePicker from "@/components/CustomDateTimePicker.vue";
|
||||
|
|
@ -396,6 +396,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
store,
|
||||
multiWindowContainerRef,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OTooltip
|
||||
:content="
|
||||
logFunctionOptions.find((o: any) => o.value === selectedFunction)
|
||||
?.tooltip || ''
|
||||
?.tooltip || raw('')
|
||||
"
|
||||
:delay="400"
|
||||
/>
|
||||
|
|
@ -317,7 +317,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OTooltip
|
||||
:content="
|
||||
logFunctionOptions.find((o: any) => o.value === selectedFunction)
|
||||
?.tooltip || ''
|
||||
?.tooltip || raw('')
|
||||
"
|
||||
:delay="400"
|
||||
/>
|
||||
|
|
@ -584,7 +584,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
/>
|
||||
<OTooltip
|
||||
v-if="cronTimezone"
|
||||
:content="cronTimezone"
|
||||
:content="raw(cronTimezone)"
|
||||
:delay="300"
|
||||
side="bottom"
|
||||
/>
|
||||
|
|
@ -964,7 +964,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
/>
|
||||
<OTooltip
|
||||
v-if="cronTimezone"
|
||||
:content="cronTimezone"
|
||||
:content="raw(cronTimezone)"
|
||||
:delay="300"
|
||||
side="bottom"
|
||||
/>
|
||||
|
|
@ -1176,7 +1176,7 @@ import {
|
|||
type Ref,
|
||||
} from "vue";
|
||||
import { type SqlErrorRange } from "@/utils/query/sqlDiagnostics";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import {
|
||||
b64EncodeUnicode,
|
||||
|
|
@ -2087,11 +2087,11 @@ export default defineComponent({
|
|||
value: "custom",
|
||||
},
|
||||
{
|
||||
label: "SQL",
|
||||
label: raw("SQL"),
|
||||
value: "sql",
|
||||
},
|
||||
{
|
||||
label: "PromQL",
|
||||
label: raw("PromQL"),
|
||||
value: "promql",
|
||||
},
|
||||
];
|
||||
|
|
@ -2104,7 +2104,7 @@ export default defineComponent({
|
|||
value: "custom",
|
||||
},
|
||||
{
|
||||
label: "SQL",
|
||||
label: raw("SQL"),
|
||||
value: "sql",
|
||||
},
|
||||
];
|
||||
|
|
@ -2770,6 +2770,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
store,
|
||||
highlightedSqlQuery,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OFormSelect
|
||||
:name="`filters[${idx}].field`"
|
||||
:options="filteredStreamFields"
|
||||
:placeholder="filter.field ? '' : t('alerts.anomaly.fieldPlaceholder')"
|
||||
:placeholder="filter.field ? raw('') : t('alerts.anomaly.fieldPlaceholder')"
|
||||
class="alert-v3-select filter-field-select"
|
||||
style="width: 200px"
|
||||
:loading="loadingFields"
|
||||
|
|
@ -111,7 +111,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:show-auto-complete="true"
|
||||
:disable-ai="!config.stream_name"
|
||||
:disable-ai-reason="
|
||||
!config.stream_name ? t('alerts.anomaly.selectStreamFirst') : ''
|
||||
!config.stream_name ? t('alerts.anomaly.selectStreamFirst') : raw('')
|
||||
"
|
||||
editor-height="100%"
|
||||
data-test="anomaly-custom-sql"
|
||||
|
|
@ -184,7 +184,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
v-if="detectionFunction && detectionFunction !== 'count'"
|
||||
name="detection_function_field"
|
||||
:options="filteredDetectionFields"
|
||||
:placeholder="detectionFunctionField ? '' : t('alerts.anomaly.fieldPlaceholder')"
|
||||
:placeholder="
|
||||
detectionFunctionField ? raw('') : t('alerts.anomaly.fieldPlaceholder')
|
||||
"
|
||||
:loading="loadingFields"
|
||||
data-test="anomaly-detection-function-field"
|
||||
class="alert-v3-select"
|
||||
|
|
@ -562,11 +564,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
label-always
|
||||
markers
|
||||
:marker-labels="[
|
||||
{ value: 0, label: '0' },
|
||||
{ value: 25, label: '25' },
|
||||
{ value: 50, label: '50' },
|
||||
{ value: 75, label: '75' },
|
||||
{ value: 100, label: '100' },
|
||||
{ value: 0, label: raw('0') },
|
||||
{ value: 25, label: raw('25') },
|
||||
{ value: 50, label: raw('50') },
|
||||
{ value: 75, label: raw('75') },
|
||||
{ value: 100, label: raw('100') },
|
||||
]"
|
||||
class="sensitivity-range-slider mt-3.5 h-36.25! [--color-slider-thumb-border:white] [--color-slider-thumb:var(--color-accent)] [--color-slider-track-fill:var(--color-accent)] [--color-slider-value:var(--color-text-secondary)]"
|
||||
data-test="anomaly-threshold-range"
|
||||
|
|
@ -583,7 +585,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, watch, type PropType } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import streamService from "@/services/stream";
|
||||
import {
|
||||
|
|
@ -647,7 +649,7 @@ export default defineComponent({
|
|||
// "SQL" stays a literal — a proper noun, not translatable copy.
|
||||
const queryTabOptions = computed(() => [
|
||||
{ label: t("alerts.queryBuilder"), value: "filters" },
|
||||
{ label: "SQL", value: "custom_sql" },
|
||||
{ label: raw("SQL"), value: "custom_sql" },
|
||||
]);
|
||||
|
||||
const filterOperators = ANOMALY_FILTER_OPERATORS;
|
||||
|
|
@ -1192,6 +1194,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
store,
|
||||
form,
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, ref } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OFormInput from "@/lib/forms/Input/OFormInput.vue";
|
||||
import OFormTextarea from "@/lib/forms/Input/OFormTextarea.vue";
|
||||
|
|
@ -281,12 +281,12 @@ export default defineComponent({
|
|||
const secretType = select((s) => s?.values?.key?.store?.akeyless?.store?.type, "");
|
||||
|
||||
const authenticationTypeOptions = [
|
||||
{ label: "Access Key", value: "access_key" },
|
||||
{ label: "LDAP", value: "ldap" },
|
||||
{ label: t("cipherKey.accessKey"), value: "access_key" },
|
||||
{ label: raw("LDAP"), value: "ldap" },
|
||||
];
|
||||
const secretTypeOptions = [
|
||||
{ label: "Static Secret", value: "static_secret" },
|
||||
{ label: "DFC", value: "dfc" },
|
||||
{ label: t("cipherKey.staticSecret"), value: "static_secret" },
|
||||
{ label: raw("DFC"), value: "dfc" },
|
||||
];
|
||||
|
||||
const getSecretOptionLabel = (value: string) =>
|
||||
|
|
@ -296,6 +296,7 @@ export default defineComponent({
|
|||
authenticationTypeOptions.find((option) => option.value === value)?.label ?? "";
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
authenticationTypeOptions,
|
||||
secretTypeOptions,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OStep
|
||||
data-test="cipher-key-key-store-detils-step"
|
||||
:name="1"
|
||||
:title="step1Title"
|
||||
:title="raw(step1Title)"
|
||||
icon="edit"
|
||||
:done="step > 1"
|
||||
>
|
||||
|
|
@ -166,7 +166,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onActivated } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import AddOpenobserveType from "@/components/cipherkeys/AddOpenobserveType.vue";
|
||||
|
|
@ -204,8 +204,8 @@ const pendingContinue = ref(false);
|
|||
const originalData = ref("");
|
||||
|
||||
const cipherKeyTypes = [
|
||||
{ label: "OpenObserve", value: "local" },
|
||||
{ label: "Akeyless", value: "akeyless" },
|
||||
{ label: raw("OpenObserve"), value: "local" },
|
||||
{ label: raw("Akeyless"), value: "akeyless" },
|
||||
];
|
||||
|
||||
const dialog = ref({
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import OFormSelect from "@/lib/forms/Select/OFormSelect.vue";
|
||||
import { FORM_CONTEXT_KEY } from "@/lib/forms/Form/OForm.types";
|
||||
|
||||
|
|
@ -66,13 +66,14 @@ export default defineComponent({
|
|||
: computed(() => "simple");
|
||||
|
||||
const providerTypeOptions = [
|
||||
{ value: "simple", label: "Simple" },
|
||||
{ value: "tink_keyset", label: "Tink KeySet" },
|
||||
{ value: "simple", label: t("cipherKeys.mechanismSimple") },
|
||||
{ value: "tink_keyset", label: raw("Tink KeySet") },
|
||||
];
|
||||
|
||||
const plainAlgorithmOptions = [{ value: "aes-256-siv", label: "AES 256 SIV" }];
|
||||
const plainAlgorithmOptions = [{ value: "aes-256-siv", label: raw("AES 256 SIV") }];
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
mechanismType,
|
||||
providerTypeOptions,
|
||||
|
|
|
|||
|
|
@ -38,12 +38,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:data-test="`tab-${tab.value}-dirty-dot`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<OTooltip v-if="tab.tooltipLabel" :content="tab.tooltipLabel" />
|
||||
<OTooltip v-if="tab.tooltipLabel" :content="raw(tab.tooltipLabel)" />
|
||||
</OToggleGroupItem>
|
||||
</OToggleGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { raw, type I18nText } from "@/types/i18n";
|
||||
import { computed } from "vue";
|
||||
import type { Component } from "vue";
|
||||
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
|
||||
|
|
@ -53,11 +54,11 @@ import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
|||
import type { ToggleGroupItemSize } from "@/lib/core/ToggleGroup/OToggleGroupItem.types";
|
||||
|
||||
interface Tab {
|
||||
label: string;
|
||||
label: I18nText;
|
||||
value: string;
|
||||
style?: Record<string, string>;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
title?: I18nText;
|
||||
tooltipLabel?: string;
|
||||
hide?: boolean;
|
||||
icon?: Component | string;
|
||||
|
|
@ -75,12 +76,12 @@ const props = withDefaults(
|
|||
activeTab: string;
|
||||
size?: ToggleGroupItemSize;
|
||||
// Tooltip shown when hovering an unsaved-changes dot (optional).
|
||||
dirtyTitle?: string;
|
||||
dirtyTitle?: I18nText;
|
||||
}>(),
|
||||
{
|
||||
show: true,
|
||||
size: "sm",
|
||||
dirtyTitle: "",
|
||||
dirtyTitle: raw(""),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OPageHeader
|
||||
v-if="!hideHeader"
|
||||
:title="title"
|
||||
:back="{ label: '', onClick: handleBack, dataTest: `${testPrefix}-import-back-btn` }"
|
||||
:back="{ label: raw(''), onClick: handleBack, dataTest: `${testPrefix}-import-back-btn` }"
|
||||
class="border-border-default shrink-0 border-b"
|
||||
:class="headerContainerClass"
|
||||
>
|
||||
|
|
@ -78,7 +78,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<AppTabs
|
||||
:data-test="`${testPrefix}-import-tabs`"
|
||||
class="tabs-selection-container"
|
||||
:tabs="tabs"
|
||||
:tabs="resolvedTabs"
|
||||
v-model:active-tab="activeTab"
|
||||
@update:active-tab="handleTabChange"
|
||||
/>
|
||||
|
|
@ -131,7 +131,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:label="t('dashboard.dropFileMsg')"
|
||||
accept=".json"
|
||||
multiple
|
||||
helpText=".json files only"
|
||||
:helpText="t('common.jsonFilesOnlyHint')"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<OIcon name="cloud-upload" size="sm" @click.stop.prevent />
|
||||
|
|
@ -207,7 +207,7 @@ import {
|
|||
onBeforeUnmount,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import axios from "axios";
|
||||
import AppTabs from "./AppTabs.vue";
|
||||
import OPageHeader from "@/lib/core/PageHeader/OPageHeader.vue";
|
||||
|
|
@ -235,26 +235,19 @@ export default defineComponent({
|
|||
props: {
|
||||
// Title for the import page
|
||||
title: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: true,
|
||||
},
|
||||
// Tabs configuration (shape matches AppTabs' Tab interface)
|
||||
// Tabs configuration (shape matches AppTabs' Tab interface).
|
||||
// No `default` here on purpose: the fallback tabs carry translated labels and
|
||||
// a prop default factory runs outside the component's i18n context, which
|
||||
// would freeze them at the locale active when the first instance mounted.
|
||||
// `resolvedTabs` in setup() builds them with `t()` instead — see below.
|
||||
tabs: {
|
||||
type: Array as PropType<
|
||||
{ label: string; value: string; icon?: string; disabled?: boolean }[]
|
||||
{ label: I18nText; value: string; icon?: string; disabled?: boolean }[]
|
||||
>,
|
||||
default: () => [
|
||||
{
|
||||
label: "File Upload / JSON",
|
||||
value: "import_json_file",
|
||||
icon: "upload",
|
||||
},
|
||||
{
|
||||
label: "URL Import",
|
||||
value: "import_json_url",
|
||||
icon: "link",
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
},
|
||||
// Default active tab
|
||||
defaultActiveTab: {
|
||||
|
|
@ -354,6 +347,17 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
|
||||
// Tabs the page actually renders: the host's `tabs` prop when given,
|
||||
// otherwise the built-in File-upload / URL pair, translated here (inside
|
||||
// setup) so the labels follow the active locale.
|
||||
const resolvedTabs = computed(
|
||||
() =>
|
||||
props.tabs ?? [
|
||||
{ label: t("common.fileUploadJsonTab"), value: "import_json_file", icon: "upload" },
|
||||
{ label: t("common.urlImportTab"), value: "import_json_url", icon: "link" },
|
||||
],
|
||||
);
|
||||
|
||||
// Computed styles
|
||||
const contentStyle = computed(() => {
|
||||
return "width: 100%;";
|
||||
|
|
@ -516,12 +520,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
jsonStr,
|
||||
jsonFiles,
|
||||
url,
|
||||
jsonArrayOfObj,
|
||||
activeTab,
|
||||
resolvedTabs,
|
||||
splitterModel,
|
||||
editorKey,
|
||||
isImporting,
|
||||
|
|
|
|||
|
|
@ -28,17 +28,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
import { computed, defineComponent } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { computed, defineComponent, type PropType } from "vue";
|
||||
import { useI18nTyped, raw, type I18nText } from "@/types/i18n";
|
||||
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "GroupHeader",
|
||||
components: { OSeparator },
|
||||
props: {
|
||||
// User-facing text: typed I18nText so a bare literal at the call site is a
|
||||
// compile error. The double cast is required because `StringConstructor`
|
||||
// returns plain `string`, which does not overlap the branded `I18nText` —
|
||||
// `<script setup>` components can use `defineProps<{ title: I18nText }>()`
|
||||
// directly and avoid this.
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
iconPath: {
|
||||
type: String,
|
||||
|
|
|
|||
|
|
@ -57,8 +57,16 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, watch, computed, defineAsyncComponent } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
onMounted,
|
||||
watch,
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import { type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
import O2AIChat from "../O2AIChat.vue";
|
||||
|
|
@ -79,7 +87,7 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
|
|
|
|||
|
|
@ -67,12 +67,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { raw, type I18nText } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import type { IconName } from "@/lib/core/Icon/OIcon.icons";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
label?: I18nText;
|
||||
/** Override the label's typography classes. Omit for the compact default. */
|
||||
labelClass?: string;
|
||||
icon?: IconName;
|
||||
|
|
@ -89,7 +90,7 @@ withDefaults(
|
|||
dataTest?: string;
|
||||
}>(),
|
||||
{
|
||||
label: "",
|
||||
label: raw(""),
|
||||
labelClass: undefined,
|
||||
icon: undefined,
|
||||
iconClass: undefined,
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useAiIcon } from "@/composables/useAiIcon";
|
||||
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
|
||||
import EmptyStateActionCard from "@/lib/core/EmptyState/EmptyStateActionCard.vue";
|
||||
|
|
@ -335,9 +335,9 @@ const props = withDefaults(
|
|||
/** Override the default "broken-panel" illustration (hero size only). */
|
||||
illustration?: IllustrationName;
|
||||
/** Override the error code's default title. */
|
||||
title?: string;
|
||||
title?: I18nText;
|
||||
/** Override the error code's default description. */
|
||||
description?: string;
|
||||
description?: I18nText;
|
||||
}>(),
|
||||
{ size: "hero", aiEnabled: false },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { raw, type I18nText } from "@/types/i18n";
|
||||
import { timestampToTimezoneDate } from "@/utils/zincutils";
|
||||
import { ref, onMounted, onBeforeUnmount, watch, computed } from "vue";
|
||||
import { ref, onMounted, onBeforeUnmount, watch, computed, type PropType } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default {
|
||||
|
|
@ -31,9 +32,9 @@ export default {
|
|||
default: null,
|
||||
},
|
||||
fullTimePrefix: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: false,
|
||||
default: "",
|
||||
default: raw(""),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
|
|
@ -110,6 +111,7 @@ export default {
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
relativeTime,
|
||||
formattedExactTime,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { I18nText } from "@/types/i18n";
|
||||
import { computed } from "vue";
|
||||
import { useRouter, type RouteLocationRaw } from "vue-router";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
|
@ -98,8 +99,8 @@ const router = useRouter();
|
|||
|
||||
export interface SectionHubItem {
|
||||
key: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
label: I18nText;
|
||||
description?: I18nText;
|
||||
icon?: string;
|
||||
to: RouteLocationRaw;
|
||||
visible?: boolean;
|
||||
|
|
@ -107,14 +108,14 @@ export interface SectionHubItem {
|
|||
}
|
||||
|
||||
export interface SectionHubGroup {
|
||||
label: string;
|
||||
label: I18nText;
|
||||
items: SectionHubItem[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
title?: I18nText;
|
||||
/** Optional one-line description shown under the hub title. */
|
||||
description?: string;
|
||||
description?: I18nText;
|
||||
groups: SectionHubGroup[];
|
||||
}>();
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { I18nText } from "@/types/i18n";
|
||||
import { computed } from "vue";
|
||||
import { useRouter, type RouteLocationRaw } from "vue-router";
|
||||
import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
|
||||
|
|
@ -92,7 +93,7 @@ const props = defineProps<{
|
|||
/** Currently-active section key (highlighted). */
|
||||
activeKey?: string;
|
||||
/** Optional small heading shown above the groups (e.g. the module name). */
|
||||
title?: string;
|
||||
title?: I18nText;
|
||||
}>();
|
||||
|
||||
// Drop hidden items/empty groups (each item may carry a `visible` flag).
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onBeforeUnmount, computed, type PropType } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
@ -85,8 +85,8 @@ export default defineComponent({
|
|||
},
|
||||
// Custom tooltip text
|
||||
tooltip: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
// Optional keyboard-shortcut hint shown in the tooltip (raw key, e.g. "ctrl+shift+c")
|
||||
shortcut: {
|
||||
|
|
@ -308,6 +308,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
isLoading,
|
||||
isWebUrlNotConfigured,
|
||||
|
|
|
|||
|
|
@ -83,13 +83,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { raw, type I18nText } from "@/types/i18n";
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
title?: I18nText;
|
||||
description?: I18nText;
|
||||
/** Optional eyebrow label rendered above the actions (e.g. "Quick start"). */
|
||||
actionsLabel?: string;
|
||||
}>(),
|
||||
{ title: "", description: "", actionsLabel: "" },
|
||||
{ title: raw(""), description: raw(""), actionsLabel: "" },
|
||||
);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -46,12 +46,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { I18nText } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import type { IconName } from "@/lib/core/Icon/OIcon.icons";
|
||||
|
||||
defineProps<{
|
||||
icon: IconName;
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
label: I18nText;
|
||||
sublabel?: I18nText;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -114,6 +114,11 @@ const mockI18n = createI18n({
|
|||
search: {
|
||||
searchField: "Search field",
|
||||
},
|
||||
// The component passes t("common.valueCopiedToClipboard") to
|
||||
// copyToClipboard; without the key this mock echoes the key path.
|
||||
common: {
|
||||
valueCopiedToClipboard: "Value copied to clipboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -654,7 +654,7 @@ const addSearchTerm = (term: string) => {
|
|||
};
|
||||
|
||||
const copyContentValue = (value: string) => {
|
||||
copyToClipboard(value, t, { successMessage: "Value copied to clipboard" });
|
||||
copyToClipboard(value, t, { successMessage: t("common.valueCopiedToClipboard") });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, computed, type PropType } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import CrossLinkUserGuide from "./CrossLinkUserGuide.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
|
|
@ -252,7 +252,7 @@ export default defineComponent({
|
|||
const added = new Set((formFields.value ?? []).map((f) => f.name));
|
||||
return (props.availableFields || [])
|
||||
.filter((name) => !added.has(name))
|
||||
.map((name) => ({ label: name, value: name }));
|
||||
.map((name) => ({ label: raw(name), value: name }));
|
||||
});
|
||||
|
||||
function onFieldSelect(value: string) {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,10 @@ describe("CrossLinkManager Component", () => {
|
|||
describe("Props Default Values", () => {
|
||||
it("should default title to 'Cross-Links'", () => {
|
||||
wrapper = createWrapper({ title: undefined });
|
||||
expect(wrapper.vm.$options.props.title.default).toBe("Cross-Links");
|
||||
// The default is no longer a literal on the prop — a literal there would ship
|
||||
// untranslated and t() at module scope would freeze the locale. It is resolved
|
||||
// per-render in setup(), so assert what the user actually sees.
|
||||
expect(wrapper.text()).toContain("Cross-Links");
|
||||
});
|
||||
|
||||
it("should default subtitle to empty string", () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<!-- Header -->
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-base font-bold">{{ title }}</div>
|
||||
<div class="text-base font-bold">{{ resolvedTitle }}</div>
|
||||
<div v-if="subtitle" class="text-text-muted text-xs">
|
||||
{{ subtitle }}
|
||||
</div>
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, type PropType } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import CrossLinkDialog from "./CrossLinkDialog.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OTag from "@/lib/core/Badge/OTag.vue";
|
||||
|
|
@ -121,12 +121,13 @@ export default defineComponent({
|
|||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "Cross-Links",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
// Resolved in setup so the fallback is translated, not frozen at load.
|
||||
default: undefined,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
|
|
@ -145,6 +146,7 @@ export default defineComponent({
|
|||
setup(props, { emit }) {
|
||||
const store = useStore();
|
||||
const { t } = useI18nTyped();
|
||||
const resolvedTitle = computed(() => props.title ?? t("common.crossLinks"));
|
||||
const showAddDialog = ref(false);
|
||||
const editingLink = ref<CrossLink | null>(null);
|
||||
const editingOriginalName = ref("");
|
||||
|
|
@ -192,6 +194,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return {
|
||||
resolvedTitle,
|
||||
|
||||
raw,
|
||||
t,
|
||||
store,
|
||||
links,
|
||||
|
|
|
|||
|
|
@ -789,7 +789,7 @@ describe("AddDashboardFromGitHub Component", () => {
|
|||
];
|
||||
await wrapper.vm.$nextTick();
|
||||
const drawer = wrapper.find('[data-test-stub="o-drawer"]');
|
||||
expect(drawer.attributes("data-primary-label")).toBe("Add 1 dashboard(s)");
|
||||
expect(drawer.attributes("data-primary-label")).toBe("Add 1 dashboard");
|
||||
});
|
||||
|
||||
it("should disable the ODrawer primary button when no dashboards selected", async () => {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
v-else-if="error"
|
||||
preset="load-error"
|
||||
size="hero"
|
||||
:description="error"
|
||||
:description="raw(error)"
|
||||
:action-label="t('dashboard.addDashboardFromGitHub.retry')"
|
||||
@action="loadDashboards"
|
||||
/>
|
||||
|
|
@ -204,7 +204,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import dashboardsService from "@/services/dashboards";
|
||||
import AddFolder from "@/components/dashboards/AddFolder.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
|
|
@ -223,7 +223,7 @@ import { toast } from "@/lib/feedback/Toast/useToast";
|
|||
interface GitHubDashboard {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
description?: I18nText;
|
||||
folderPath: string;
|
||||
jsonFiles: string[];
|
||||
}
|
||||
|
|
@ -266,7 +266,7 @@ export default defineComponent({
|
|||
const selectedDashboards = ref<GitHubDashboard[]>([]);
|
||||
const showFolderSelection = ref(false);
|
||||
const selectedFolderObj = ref<string | null>(null);
|
||||
const folderOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const folderOptions = ref<{ label: I18nText; value: string }[]>([]);
|
||||
const importing = ref(false);
|
||||
const preparing = ref(false);
|
||||
const showAddFolderDialog = ref(false);
|
||||
|
|
@ -434,7 +434,7 @@ export default defineComponent({
|
|||
];
|
||||
|
||||
folderOptions.value = sorted.map((f: any) => ({
|
||||
label: f.name,
|
||||
label: raw(f.name),
|
||||
value: f.folderId,
|
||||
}));
|
||||
|
||||
|
|
@ -656,6 +656,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
show,
|
||||
loading,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import type { I18nText } from "@/types/i18n";
|
||||
|
||||
import type { Ref } from "vue";
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -97,8 +99,8 @@ export interface PanelEditorChartData {
|
|||
version?: number;
|
||||
id?: string;
|
||||
type?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
title?: I18nText;
|
||||
description?: I18nText;
|
||||
config?: Record<string, any>;
|
||||
htmlContent?: string;
|
||||
markdownContent?: string;
|
||||
|
|
@ -152,8 +154,8 @@ export interface PanelEditorChartData {
|
|||
*/
|
||||
export interface PanelEditorDashboardData {
|
||||
dashboardId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
title?: I18nText;
|
||||
description?: I18nText;
|
||||
tabs?: any[];
|
||||
variables?: {
|
||||
list?: any[];
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
</div>
|
||||
<div class="group relative mt-1">
|
||||
<div
|
||||
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] [scrollbar-width:thin]"
|
||||
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 [scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap"
|
||||
:data-test="`query-inspector-original-query-${index}`"
|
||||
v-html="
|
||||
highlightSearch(
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
</div>
|
||||
<div class="group relative mt-1">
|
||||
<div
|
||||
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] [scrollbar-width:thin]"
|
||||
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 [scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap"
|
||||
:data-test="`query-inspector-executed-query-${index}`"
|
||||
v-html="
|
||||
highlightSearch(
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
import { ref, computed, watch } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped, raw } from "@/types/i18n";
|
||||
import { useI18nTyped, raw, type I18nText } from "@/types/i18n";
|
||||
import { useLoading } from "@/composables/useLoading";
|
||||
import { annotationService } from "@/services/dashboard_annotations";
|
||||
import useNotifications from "@/composables/useNotifications";
|
||||
|
|
@ -95,7 +95,7 @@ import type { AddAnnotationForm } from "./AddAnnotation.schema";
|
|||
|
||||
interface AnnotationData {
|
||||
annotation_id: string | null;
|
||||
title: string;
|
||||
title: I18nText;
|
||||
text: string;
|
||||
start_time: number | null;
|
||||
end_time: number | null;
|
||||
|
|
@ -105,7 +105,7 @@ interface AnnotationData {
|
|||
|
||||
interface AnnotationPanel {
|
||||
id: string;
|
||||
title: string;
|
||||
title: I18nText;
|
||||
tabName?: string;
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ const showDeleteConfirm = ref(false);
|
|||
const annotationData = ref<AnnotationData>(
|
||||
props.annotation || {
|
||||
annotation_id: null,
|
||||
title: "",
|
||||
title: raw(""),
|
||||
text: "",
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
|
|
@ -158,9 +158,9 @@ const groupedPanels = ref<Record<string, AnnotationPanel[]>>({});
|
|||
|
||||
const groupedPanelsOptions = computed(() =>
|
||||
Object.entries(groupedPanels.value).flatMap(([tab, panels]) => [
|
||||
{ label: tab, isTab: true, disable: true },
|
||||
{ label: raw(tab), isTab: true, disable: true },
|
||||
...panels.map((panel) => ({
|
||||
label: panel.title,
|
||||
label: raw(panel.title),
|
||||
value: panel.id,
|
||||
isTab: false,
|
||||
})),
|
||||
|
|
@ -286,7 +286,7 @@ const confirmDelete = async () => {
|
|||
// reads consistent values. Plain async — OForm awaits it, and the ODialog
|
||||
// built-in primary button (form-id) auto-shows the Save spinner (no useLoading).
|
||||
const saveAnnotation = async (value: AddAnnotationForm) => {
|
||||
if (value?.title != null) annotationData.value.title = value.title;
|
||||
if (value?.title != null) annotationData.value.title = raw(value.title);
|
||||
annotationData.value.text = value?.text ?? "";
|
||||
annotationData.value.panels = value?.panels ?? [];
|
||||
await handleSave();
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { I18nText } from "@/types/i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** Full chip label, e.g. "histogram(_timestamp, acos(actual_value))" */
|
||||
label: string;
|
||||
label: I18nText;
|
||||
/** Token utility for every function/aggregation name (nested included) */
|
||||
fnClass?: string;
|
||||
/** Token utility for the emphasized field/column name */
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ import useDashboardPanelData from "@/composables/dashboard/useDashboardPanel";
|
|||
import { getColorPalette } from "@/utils/dashboard/colorPalette";
|
||||
import { computed, inject, onBeforeMount, defineComponent } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
|
||||
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
|
||||
import OSelect from "@/lib/forms/Select/OSelect.vue";
|
||||
|
|
@ -153,9 +153,9 @@ import OSelectGroup from "@/lib/forms/Select/OSelectGroup.vue";
|
|||
import type { SelectModelValue } from "@/lib/forms/Select/OSelect.types";
|
||||
|
||||
interface ColorOption {
|
||||
label: string;
|
||||
label: I18nText;
|
||||
value?: string;
|
||||
subLabel?: string;
|
||||
subLabel?: I18nText;
|
||||
colorPalette?: string[];
|
||||
header?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-test="dashboard-config-panel-time-picker"
|
||||
class="w-fit max-w-full min-w-0 overflow-hidden"
|
||||
/>
|
||||
<OTooltip :content="formattedPickerValue" max-width="320px" />
|
||||
<OTooltip :content="raw(formattedPickerValue)" max-width="320px" />
|
||||
</div>
|
||||
<OIcon
|
||||
class="mr-1 ml-2 flex-shrink-0 shrink-0 cursor-pointer"
|
||||
|
|
@ -440,7 +440,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
v-for="(tab, index) in dashboardPanelData.data.queries"
|
||||
:key="index"
|
||||
:name="index"
|
||||
:label="`${t('dashboard.queryLabel')} ${Number(index) + 1}`"
|
||||
:label="raw(`${t('dashboard.queryLabel')} ${Number(index) + 1}`)"
|
||||
:data-test="`dashboard-config-query-tab-${index}`"
|
||||
>
|
||||
</OTab>
|
||||
|
|
@ -603,7 +603,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
/>
|
||||
</div>
|
||||
<OInput
|
||||
placeholder="{field_name}"
|
||||
:placeholder="raw('{field_name}')"
|
||||
v-model="
|
||||
dashboardPanelDataModel.data.queries[dashboardPanelData.layout.currentQueryIndex]
|
||||
.config.query_label
|
||||
|
|
@ -719,7 +719,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
v-if="shouldShowNoValueReplacement(dashboardPanelData, promqlMode)"
|
||||
v-show="isConfigOptionVisible('data', 'no-value-replacement')"
|
||||
v-model="dashboardPanelDataModel.data.config.no_value_replacement"
|
||||
placeholder="-"
|
||||
:placeholder="raw('-')"
|
||||
:label="t('dashboard.noValueReplacement')"
|
||||
data-test="dashboard-config-no-value-replacement"
|
||||
>
|
||||
|
|
@ -1729,7 +1729,7 @@ import { type SwitchValue } from "@/lib/forms/Switch/OSwitch.types";
|
|||
import useDashboardPanelData from "@/composables/dashboard/useDashboardPanel";
|
||||
import { getUnitOptions } from "@/composables/dashboard/useColumnFormatting";
|
||||
import { computed, defineComponent, inject, nextTick, onBeforeMount, onMounted, ref } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import Drilldown from "./Drilldown.vue";
|
||||
import ValueMapping from "./ValueMapping.vue";
|
||||
import ColorBySeries from "./ColorBySeries.vue";
|
||||
|
|
@ -2150,8 +2150,10 @@ export default defineComponent({
|
|||
value: "center",
|
||||
},
|
||||
];
|
||||
// Single source of truth — shared with the column-formatting dialog.
|
||||
const unitOptions = getUnitOptions(t);
|
||||
// Single source of truth — shared with the column-formatting dialog. Its
|
||||
// labels are already translated (it is handed `t`), but the helper's
|
||||
// signature widens them back to `string`, so re-brand them for OSelect.
|
||||
const unitOptions = getUnitOptions(t).map((o) => ({ ...o, label: raw(o.label) }));
|
||||
|
||||
const labelPositionOptions = [
|
||||
{
|
||||
|
|
@ -2306,7 +2308,7 @@ export default defineComponent({
|
|||
|
||||
return streamFields.schema.map((it: any) => {
|
||||
return {
|
||||
label: it.name,
|
||||
label: raw(it.name),
|
||||
value: it.name,
|
||||
};
|
||||
});
|
||||
|
|
@ -2557,6 +2559,7 @@ export default defineComponent({
|
|||
const decimalsTouched = ref(false);
|
||||
|
||||
return {
|
||||
raw,
|
||||
legendsPositionModel,
|
||||
legendsTypeModel,
|
||||
chartAlignModel,
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
"
|
||||
:hide-nl-toggle="false"
|
||||
:disable-ai="false"
|
||||
:disable-ai-reason="''"
|
||||
:disable-ai-reason="raw('')"
|
||||
:ai-placeholder="t('function.askAIFunctionPlaceholder')"
|
||||
:ai-tooltip="t('function.enterFunctionPrompt')"
|
||||
editor-height="100%"
|
||||
|
|
@ -276,7 +276,7 @@ import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
|
|||
import OTab from "@/lib/navigation/Tabs/OTab.vue";
|
||||
// @ts-nocheck
|
||||
import { defineComponent, ref, watch, computed, onMounted, nextTick, onUnmounted } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import useDashboardPanelData from "../../../composables/dashboard/useDashboardPanel";
|
||||
import QueryTypeSelector from "../addPanel/QueryTypeSelector.vue";
|
||||
|
|
@ -892,6 +892,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
router,
|
||||
onDropDownClick,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
class="user-guide border-border-default rounded-default bg-surface-base pointer-events-auto fixed z-9999 max-h-75 w-125 overflow-y-auto border p-2.5 [scrollbar-color:color-mix(in_srgb,var(--color-grey-950)_25%,transparent)_color-mix(in_srgb,var(--color-grey-950)_5%,transparent)] [scrollbar-width:thin] [&_div]:m-0 [&_li]:m-0 [&_p]:m-0 [&_ul]:m-0"
|
||||
class="user-guide border-border-default rounded-default bg-surface-base pointer-events-auto fixed z-9999 max-h-75 w-125 [scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--color-grey-950)_25%,transparent)_color-mix(in_srgb,var(--color-grey-950)_5%,transparent)] overflow-y-auto border p-2.5 [&_div]:m-0 [&_li]:m-0 [&_p]:m-0 [&_ul]:m-0"
|
||||
v-show="showUserGuide"
|
||||
@mouseleave="showUserGuide = false"
|
||||
ref="userGuideDivRef"
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, inject, onBeforeMount } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OverrideConfigPopup from "../OverrideConfigPopup.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import useDashboardPanelData from "../../../composables/dashboard/useDashboardPanel";
|
||||
|
||||
interface Column {
|
||||
alias: string;
|
||||
label: string;
|
||||
label: I18nText;
|
||||
format?: (val: unknown) => string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted, inject } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import useDashboardPanelData from "@/composables/dashboard/useDashboardPanel";
|
||||
|
|
@ -515,7 +515,7 @@ function onStreamChange(val: SelectModelValue) {
|
|||
// ── Stream type options ────────────────────────────────────────────────
|
||||
|
||||
const streamTypeOptions = computed(() =>
|
||||
["logs", "metrics", "traces"].map((t: string) => ({ label: t, value: t })),
|
||||
["logs", "metrics", "traces"].map((t: string) => ({ label: raw(t), value: t })),
|
||||
);
|
||||
|
||||
// ── Stream list ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, provide } from "vue";
|
||||
import { type I18nText } from "@/types/i18n";
|
||||
import { defineComponent, ref, watch, provide, type PropType } from "vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
|
||||
|
|
@ -80,7 +81,7 @@ export default defineComponent({
|
|||
components: { OSeparator, OButton, OIcon },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ const mockI18n = createI18n({
|
|||
locale: "en",
|
||||
messages: {
|
||||
en: {
|
||||
common: {
|
||||
all: "All",
|
||||
},
|
||||
dashboard: {
|
||||
rowsPerPage: "Rows per page",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OSelect from "@/lib/forms/Select/OSelect.vue";
|
||||
import type { SelectModelValue } from "@/lib/forms/Select/OSelect.types";
|
||||
|
|
@ -140,12 +140,13 @@ export default defineComponent({
|
|||
|
||||
const formattedPaginationOptions = computed(() =>
|
||||
props.paginationOptions.map((opt) => ({
|
||||
label: opt === 0 ? "All" : String(opt),
|
||||
label: opt === 0 ? t("common.all") : raw(String(opt)),
|
||||
value: opt,
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
raw,
|
||||
countDisplay,
|
||||
formattedPaginationOptions,
|
||||
t,
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { defineComponent, ref, computed } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { defineComponent, ref, computed, type PropType } from "vue";
|
||||
import { type I18nText, useI18nTyped } from "@/types/i18n";
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
|
||||
import OBanner from "@/lib/feedback/Banner/OBanner.vue";
|
||||
|
|
@ -58,11 +58,11 @@ export default defineComponent({
|
|||
emits: ["update:ok", "update:cancel", "update:modelValue"],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: true,
|
||||
},
|
||||
warningMessage: {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
>
|
||||
<img
|
||||
:src="chart.asset"
|
||||
:alt="chart.label"
|
||||
:alt="t(chart.labelKey)"
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
|
@ -139,7 +139,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</OCardSection>
|
||||
<OCardSection class="px-2 pt-0 pb-2">
|
||||
<div class="text-center text-xs font-medium">
|
||||
{{ chart.label }}
|
||||
{{ t(chart.labelKey) }}
|
||||
</div>
|
||||
</OCardSection>
|
||||
</OCard>
|
||||
|
|
@ -218,8 +218,10 @@ export default defineComponent({
|
|||
const filtered: ChartCategory[] = [];
|
||||
|
||||
chartCategories.value.forEach((category) => {
|
||||
// Search the RESOLVED name, not the key — otherwise typing "line" would
|
||||
// match the dotted path rather than what the user can actually see.
|
||||
const filteredCharts = category.type.filter((chart) =>
|
||||
chart.label.toLowerCase().includes(query),
|
||||
t(chart.labelKey).toLowerCase().includes(query),
|
||||
);
|
||||
|
||||
if (filteredCharts.length > 0) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import type { I18nKey } from "@/types/i18n";
|
||||
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
|
||||
export interface ChartType {
|
||||
label: string;
|
||||
/**
|
||||
* i18n KEY for the example's display name, not the English text — this module
|
||||
* is a plain, module-scope constant with no `t()` in reach, so resolving here
|
||||
* would freeze the copy at whatever locale was active on import. The consumer
|
||||
* (CustomChartTypeSelector.vue) calls `t(labelKey)` at render time instead.
|
||||
*/
|
||||
labelKey: I18nKey;
|
||||
value: string;
|
||||
asset: string;
|
||||
}
|
||||
|
|
@ -17,22 +25,22 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Line",
|
||||
type: [
|
||||
{
|
||||
label: "Basic Line Chart",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.basicLineChart",
|
||||
value: "line-simple",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/line-simple.webp"),
|
||||
},
|
||||
{
|
||||
label: "Confidence Band",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.confidenceBand",
|
||||
value: "confidence-band",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/confidence-band.webp"),
|
||||
},
|
||||
{
|
||||
label: "Multiple X Axes",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.multipleXAxes",
|
||||
value: "multiple-x-axis",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/multiple-x-axis.webp"),
|
||||
},
|
||||
{
|
||||
label: "Intraday Line Breaks 1",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.intradayLineBreaks1",
|
||||
value: "intraday-breaks-1",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/intraday-breaks-1.webp"),
|
||||
},
|
||||
|
|
@ -42,17 +50,17 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Bar",
|
||||
type: [
|
||||
{
|
||||
label: "Bar Race",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.barRace",
|
||||
value: "bar-race",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/bar-race.webp"),
|
||||
},
|
||||
{
|
||||
label: "Stacked Bar Normalization",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.stackedBarNormalization",
|
||||
value: "bar-stack-normalization",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/bar-stack-normalization.webp"),
|
||||
},
|
||||
{
|
||||
label: "Stacked Radial Bar (Polar)",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.stackedRadialBarPolar",
|
||||
value: "bar-polar-stack-radial",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/bar-polar-stack-radial.webp"),
|
||||
},
|
||||
|
|
@ -62,12 +70,12 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Pie",
|
||||
type: [
|
||||
{
|
||||
label: "Pie with Border",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.pieWithBorder",
|
||||
value: "pie-border-radius",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/pie-border-radius.webp"),
|
||||
},
|
||||
{
|
||||
label: "Partition Data to Pies",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.partitionDataToPies",
|
||||
value: "data-transform-multiple-pie",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/data-transform-multiple-pie.webp"),
|
||||
},
|
||||
|
|
@ -77,12 +85,12 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Scatter",
|
||||
type: [
|
||||
{
|
||||
label: "Scatter Matrix",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.scatterMatrix",
|
||||
value: "scatter-matrix",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/scatter-matrix.webp"),
|
||||
},
|
||||
{
|
||||
label: "Scatter Polynomial Regression",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.scatterPolynomialRegression",
|
||||
value: "scatter-polynomial-regression",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/scatter-polynomial-regression.webp"),
|
||||
},
|
||||
|
|
@ -92,12 +100,12 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Radar",
|
||||
type: [
|
||||
{
|
||||
label: "Customized Radar Chart",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.customizedRadarChart",
|
||||
value: "radar-custom",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/radar-custom.webp"),
|
||||
},
|
||||
{
|
||||
label: "Multiple Radar",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.multipleRadar",
|
||||
value: "radar-multiple-2",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/radar-multiple-2.webp"),
|
||||
},
|
||||
|
|
@ -107,7 +115,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Boxplot",
|
||||
type: [
|
||||
{
|
||||
label: "Data Transform Simple Aggregate",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.dataTransformSimpleAggregate",
|
||||
value: "data-transform-aggregate",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/data-transform-aggregate.webp"),
|
||||
},
|
||||
|
|
@ -117,7 +125,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Graph",
|
||||
type: [
|
||||
{
|
||||
label: "Graph on Cartesian",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.graphOnCartesian",
|
||||
value: "graph-on-cartesian",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/graph-on-cartesian.webp"),
|
||||
},
|
||||
|
|
@ -127,7 +135,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Treemap",
|
||||
type: [
|
||||
{
|
||||
label: "Treemap chart",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.treemapChart",
|
||||
value: "treemap-chart",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/treemap-chart.webp"),
|
||||
},
|
||||
|
|
@ -137,7 +145,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Funnel",
|
||||
type: [
|
||||
{
|
||||
label: "Customized Funnel",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.customizedFunnel",
|
||||
value: "funnel-customize",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/funnel-customize.webp"),
|
||||
},
|
||||
|
|
@ -147,7 +155,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "Dataset",
|
||||
type: [
|
||||
{
|
||||
label: "Series Layout",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.seriesLayout",
|
||||
value: "dataset-series-layout",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/dataset-series-layout.webp"),
|
||||
},
|
||||
|
|
@ -157,22 +165,22 @@ export const chartTypesData: { data: ChartCategory[] } = {
|
|||
chartLabel: "3D",
|
||||
type: [
|
||||
{
|
||||
label: "3D Bar with Dataset",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.threeDBarWithDataset",
|
||||
value: "bar3d-dataset",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/bar3d-dataset.webp"),
|
||||
},
|
||||
{
|
||||
label: "Bar3D Punch Card",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.bar3dPunchCard",
|
||||
value: "bar3d-punchcard",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/bar3d-punchcard.webp"),
|
||||
},
|
||||
{
|
||||
label: "3D Scatter with Scatter Matrix",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.threeDScatterWithScatterMatrix",
|
||||
value: "scatter3d-scatter-matrix",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/scatter3d-scatter-matrix.webp"),
|
||||
},
|
||||
{
|
||||
label: "3D Scatter Dataset",
|
||||
labelKey: "dashboard.customChartTypeSelector.examples.threeDScatterDataset",
|
||||
value: "scatter3d-dataset",
|
||||
asset: getImageURL("dashboard/CustomChartAssets/scatter3D-dataset.webp"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<DashboardHeader :title="title" backButton @back="close"> </DashboardHeader>
|
||||
|
||||
<div
|
||||
class="[&::-webkit-scrollbar-thumb]:rounded-default [&::-webkit-scrollbar-thumb]:bg-border-default min-h-0 flex-1 overflow-y-auto px-0.75 pb-4 [scrollbar-color:var(--color-border-default)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:bg-transparent"
|
||||
class="[&::-webkit-scrollbar-thumb]:rounded-default [&::-webkit-scrollbar-thumb]:bg-border-default min-h-0 flex-1 [scrollbar-width:thin] [scrollbar-color:var(--color-border-default)_transparent] overflow-y-auto px-0.75 pb-4 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:bg-transparent"
|
||||
>
|
||||
<OForm greedy id="add-setting-variable-form" :form="form" class="px-0.5">
|
||||
<div class="mt-3">
|
||||
|
|
@ -204,7 +204,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
valueKey="name"
|
||||
searchable
|
||||
:placeholder="
|
||||
filter.name ? '' : t('dashboard.addSettingVariable.selectFieldPlaceholder')
|
||||
filter.name
|
||||
? raw('')
|
||||
: t('dashboard.addSettingVariable.selectFieldPlaceholder')
|
||||
"
|
||||
:title="filter.name || undefined"
|
||||
@update:model-value="filterUpdated(index, $event)"
|
||||
|
|
@ -529,7 +531,7 @@ import {
|
|||
computed,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useSelectAutoComplete } from "../../../composables/useSelectAutocomplete";
|
||||
import { useStore } from "vuex";
|
||||
import { addVariable, getDashboard, updateVariable } from "../../../utils/commons";
|
||||
|
|
@ -607,7 +609,7 @@ export default defineComponent({
|
|||
filter: [],
|
||||
},
|
||||
value: "",
|
||||
options: [{ label: "", value: "", selected: true }],
|
||||
options: [{ label: raw(""), value: "", selected: true }],
|
||||
multiSelect: false,
|
||||
hideOnDashboard: false,
|
||||
selectAllValueForMultiSelect: "first",
|
||||
|
|
@ -689,7 +691,7 @@ export default defineComponent({
|
|||
// Format tabs for selection from dashboard data
|
||||
const tabsOptions = computed(() =>
|
||||
dashboardData.value.tabs.map((tab: any) => ({
|
||||
label: tab.name,
|
||||
label: raw(tab.name),
|
||||
value: tab.tabId,
|
||||
})),
|
||||
);
|
||||
|
|
@ -730,7 +732,7 @@ export default defineComponent({
|
|||
// Add existing panels from this tab
|
||||
panelOptions.push(
|
||||
...(tab.panels || []).map((panel: any) => ({
|
||||
label: panel.title,
|
||||
label: raw(panel.title),
|
||||
value: panel.id,
|
||||
})),
|
||||
);
|
||||
|
|
@ -788,7 +790,7 @@ export default defineComponent({
|
|||
]);
|
||||
|
||||
const streamTypeOptions = computed(() =>
|
||||
data.streamType.map((t: string) => ({ label: t, value: t })),
|
||||
data.streamType.map((t: string) => ({ label: raw(t), value: t })),
|
||||
);
|
||||
|
||||
const handleCustomSelectAll = () => {
|
||||
|
|
@ -1051,7 +1053,7 @@ export default defineComponent({
|
|||
const addField = () => {
|
||||
// add new field for options
|
||||
formPush("options", {
|
||||
label: "",
|
||||
label: raw(""),
|
||||
value: "",
|
||||
selected: false,
|
||||
});
|
||||
|
|
@ -1442,7 +1444,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return filteredVars.map((it: any) => ({
|
||||
label: it.name,
|
||||
label: raw(it.name),
|
||||
value: "$" + it.name,
|
||||
}));
|
||||
});
|
||||
|
|
@ -1539,6 +1541,7 @@ export default defineComponent({
|
|||
isSavingVariable,
|
||||
store,
|
||||
t,
|
||||
raw,
|
||||
data,
|
||||
streamsFilterFn,
|
||||
fieldsFilterFn,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
import { defineComponent, ref, toRef, watch, type Ref, toRefs } from "vue";
|
||||
import { useSelectAutoComplete } from "../../../composables/useSelectAutocomplete";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import DynamicFilterIcon from "../../icons/DynamicFilterIcon.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OInput from "@/lib/forms/Input/OInput.vue";
|
||||
|
|
@ -79,8 +79,8 @@ export default defineComponent({
|
|||
const store = useStore();
|
||||
const { t } = useI18nTyped();
|
||||
const operatorOptions = [
|
||||
{ label: "=", value: "=" },
|
||||
{ label: "!=", value: "!=" },
|
||||
{ label: raw("="), value: "=" },
|
||||
{ label: raw("!="), value: "!=" },
|
||||
];
|
||||
const options = toRef(props.variableItem, "options");
|
||||
const { modelValue: adhocVariables } = toRefs(props) as {
|
||||
|
|
@ -121,6 +121,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
fieldsFilterFn,
|
||||
fieldsFilteredOptions,
|
||||
addFields,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { raw, type I18nText } from "@/types/i18n";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
|
||||
|
||||
|
|
@ -51,8 +52,8 @@ export default defineComponent({
|
|||
components: { OSeparator, OButton },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
default: raw(""),
|
||||
},
|
||||
backButton: {
|
||||
type: Boolean,
|
||||
|
|
@ -66,6 +67,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
onBackClicked,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
testPrefix — data-test prefix, so each editor keeps its existing selectors
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { I18nText } from "@/types/i18n";
|
||||
// Palette styling lives in its own token-driven stylesheet (see the note at the
|
||||
// top of node-palette.css for why it cannot be scoped). Imported here rather
|
||||
// than through an SFC style block, so this component carries none at all.
|
||||
|
|
@ -51,8 +52,8 @@ interface NodePaletteItem {
|
|||
subtype?: string;
|
||||
io_type?: string;
|
||||
isSectionHeader?: boolean;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
label?: I18nText;
|
||||
tooltip?: I18nText;
|
||||
icon?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<OSearchInput
|
||||
ref="searchRef"
|
||||
v-model="search"
|
||||
:placeholder="placeholderText"
|
||||
:placeholder="raw(placeholderText)"
|
||||
clearable
|
||||
class="w-full"
|
||||
:data-test="testPrefix + '-search'"
|
||||
|
|
@ -103,14 +103,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
|
||||
|
||||
interface StepItem {
|
||||
key: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
title: I18nText;
|
||||
description?: I18nText;
|
||||
icon?: string;
|
||||
iconTint?: string;
|
||||
[k: string]: any;
|
||||
|
|
@ -119,8 +119,8 @@ interface StepItem {
|
|||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: StepItem[];
|
||||
searchPlaceholder?: string;
|
||||
noMatchText?: string;
|
||||
searchPlaceholder?: I18nText;
|
||||
noMatchText?: I18nText;
|
||||
testPrefix?: string;
|
||||
/** Viewport point to open at (usually the click). Null = screen-centred. */
|
||||
anchor?: { x: number; y: number } | null;
|
||||
|
|
@ -130,8 +130,8 @@ const props = withDefaults(
|
|||
// Empty, not English: t() cannot run at module scope (no setup context), so
|
||||
// the locale fallback lives in the computeds below. A caller may still pass
|
||||
// its own already-translated string.
|
||||
searchPlaceholder: "",
|
||||
noMatchText: "",
|
||||
searchPlaceholder: raw(""),
|
||||
noMatchText: raw(""),
|
||||
testPrefix: "flow-step",
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -369,16 +369,16 @@ export default defineComponent({
|
|||
if (hasFailedJob.value) {
|
||||
// When there's a failed job, only allow: reload, replace failed, or replace all
|
||||
return [
|
||||
{ label: "Reload existing URLs", value: "reload" },
|
||||
{ label: "Replace failed URL only", value: "replace_failed" },
|
||||
{ label: "Replace all URLs", value: "replace" },
|
||||
{ label: t("function.updateModeReload"), value: "reload" },
|
||||
{ label: t("function.updateModeReplaceFailed"), value: "replace_failed" },
|
||||
{ label: t("function.updateModeReplaceAll"), value: "replace" },
|
||||
];
|
||||
} else {
|
||||
// Normal mode: reload, append, or replace
|
||||
return [
|
||||
{ label: "Reload existing URLs", value: "reload" },
|
||||
{ label: "Add new URL", value: "append" },
|
||||
{ label: "Replace all URLs", value: "replace" },
|
||||
{ label: t("function.updateModeReload"), value: "reload" },
|
||||
{ label: t("function.updateModeAppend"), value: "append" },
|
||||
{ label: t("function.updateModeReplaceAll"), value: "replace" },
|
||||
];
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
name="function"
|
||||
v-model:is-expanded="expandState.functions"
|
||||
:label="
|
||||
(transType === '1' ? t('function.jsfunction') : t('function.vrlfunction')) + '*'
|
||||
raw(
|
||||
(transType === '1' ? t('function.jsfunction') : t('function.vrlfunction')) +
|
||||
'*',
|
||||
)
|
||||
"
|
||||
min-header-height="2.125rem"
|
||||
/>
|
||||
|
|
@ -73,7 +76,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:query="formData.function"
|
||||
:hide-nl-toggle="!store.state.zoConfig.ai_enabled"
|
||||
:disable-ai="!store.state.zoConfig.ai_enabled"
|
||||
:disable-ai-reason="''"
|
||||
:disable-ai-reason="raw('')"
|
||||
:ai-placeholder="t('function.askAIFunctionPlaceholder')"
|
||||
:ai-tooltip="t('function.enterFunctionPrompt')"
|
||||
editor-height="100%"
|
||||
|
|
@ -182,7 +185,7 @@ import {
|
|||
} from "vue";
|
||||
|
||||
import jsTransformService from "../../services/jstransform";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import config from "@/aws-exports";
|
||||
import segment from "../../services/segment_analytics";
|
||||
|
|
@ -597,6 +600,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
return {
|
||||
raw,
|
||||
t,
|
||||
emit,
|
||||
disableColor,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@
|
|||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { type I18nText } from "@/types/i18n";
|
||||
import { computed, type PropType } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
type: String as unknown as PropType<I18nText>,
|
||||
required: true,
|
||||
},
|
||||
isExpandable: {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@
|
|||
class="text-status-error-text mx-1 cursor-pointer"
|
||||
size="sm"
|
||||
>
|
||||
<OTooltip side="right" align="center" :side-offset="10" :content="sqlQueryErrorMsg" />
|
||||
<OTooltip
|
||||
side="right"
|
||||
align="center"
|
||||
:side-offset="10"
|
||||
:content="raw(sqlQueryErrorMsg)"
|
||||
/>
|
||||
</OIcon>
|
||||
</template>
|
||||
<template #right>
|
||||
|
|
@ -142,7 +147,12 @@
|
|||
class="text-status-error-text mx-1 cursor-pointer"
|
||||
size="sm"
|
||||
>
|
||||
<OTooltip side="right" align="center" :side-offset="10" :content="eventsErrorMsg" />
|
||||
<OTooltip
|
||||
side="right"
|
||||
align="center"
|
||||
:side-offset="10"
|
||||
:content="raw(eventsErrorMsg)"
|
||||
/>
|
||||
</OIcon>
|
||||
</template>
|
||||
<template #right>
|
||||
|
|
@ -206,7 +216,7 @@
|
|||
side="right"
|
||||
align="center"
|
||||
:side-offset="10"
|
||||
:content="outputEventsErrorMsg"
|
||||
:content="raw(outputEventsErrorMsg)"
|
||||
/>
|
||||
</OIcon>
|
||||
</template>
|
||||
|
|
@ -361,9 +371,9 @@ const { getStreams, getStream } = useStreams(t);
|
|||
const { buildQueryPayload } = useQuery();
|
||||
|
||||
const streamTypes = [
|
||||
{ label: "Logs", value: "logs", icon: "description" },
|
||||
{ label: "Metrics", value: "metrics", icon: "bar-chart" },
|
||||
{ label: "Traces", value: "traces", icon: "activity" },
|
||||
{ label: t("common.logs"), value: "logs", icon: "description" },
|
||||
{ label: t("common.metrics"), value: "metrics", icon: "bar-chart" },
|
||||
{ label: t("common.traces"), value: "traces", icon: "activity" },
|
||||
];
|
||||
|
||||
const isFetchingStreams = ref(false);
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<script lang="ts">
|
||||
import { ref, computed, defineComponent, onBeforeMount } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
|
@ -240,7 +240,7 @@ import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
|
|||
interface Token {
|
||||
name: string;
|
||||
token: string;
|
||||
description: string;
|
||||
description: I18nText;
|
||||
is_default: boolean;
|
||||
enabled: boolean;
|
||||
created_by: string;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<template>
|
||||
<OPageLayout
|
||||
data-test="edit-group-section"
|
||||
:title="groupDetails.group_name"
|
||||
:title="raw(groupDetails.group_name)"
|
||||
:back="{ label: t('iam.groups'), onClick: cancelEditGroup }"
|
||||
bleed
|
||||
>
|
||||
|
|
@ -96,7 +96,7 @@ import GroupRoles from "./GroupRoles.vue";
|
|||
import GroupUsers from "./GroupUsers.vue";
|
||||
import AppTabs from "@/components/common/AppTabs.vue";
|
||||
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import { useRouter, onBeforeRouteLeave } from "vue-router";
|
||||
import { onBeforeMount } from "vue";
|
||||
import { getGroup, updateGroup } from "@/services/iam";
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue