fix: Issue 12803 language translation fix (#12839)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Bhargav
2026-06-24 10:03:43 +00:00
committed by GitHub
co-authored by github-actions[bot]
parent 2975f75b92
commit c494fe93f9
60 changed files with 14785 additions and 902 deletions
+5 -4
View File
@@ -1274,7 +1274,7 @@
size="sm"
:class="store.state.theme == 'dark' ? 'tw:text-white' : 'tw:text-gray-600'"
/>
<OTooltip content="Attach images (PNG, JPEG, max 2MB)" />
<OTooltip :content="t('aiAssistant.attachImageTooltip')" />
</OButton>
<div v-else class="tw:w-8"></div>
@@ -1301,12 +1301,12 @@
: ''
]"
/>
<span class="auto-nav-label tw:ml-1">Auto Navigation</span>
<span class="auto-nav-label tw:ml-1">{{ t('aiAssistant.autoNavigation.label') }}</span>
<OTooltip
:content="
isAutoNavigationEnabled
? 'Auto navigation enabled - O2 Assistant will auto navigate without confirmation'
: 'Auto navigation disabled - O2 Assistant will ask before navigating'
? t('aiAssistant.autoNavigation.enabledTooltip')
: t('aiAssistant.autoNavigation.disabledTooltip')
"
/>
</OButton>
@@ -5693,6 +5693,7 @@ export default defineComponent({
closeImagePreview,
contextReferences,
handleReferencesUpdate,
t,
};
},
});
+2 -2
View File
@@ -113,7 +113,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="card-container tw:shrink-0 stream-config-card">
<div class="section-header">
<div class="section-header-accent" />
<span class="section-header-title">Stream Config <span class="tw:text-text-primary">*</span></span>
<span class="section-header-title">{{ t('alerts.streamConfig') }} <span class="tw:text-text-primary">*</span></span>
</div>
<div class="tw:flex tw:items-center tw:gap-4 tw:px-3 tw:py-2">
<!-- Stream Type -->
@@ -551,7 +551,7 @@ export default defineComponent({
{ key: "anomaly-alerting", label: alertForm.t("alerts.alerting") || "Alerting", required: alertForm.anomalyConfig.value.alert_enabled },
]
: [
{ key: "condition", label: "Alert Rules", required: true },
{ key: "condition", label: alertForm.t("alerts.alertRules"), required: true },
{ key: "advanced", label: alertForm.t("alerts.steps.advanced") },
];
return tabs.filter((tab: any) => (tab as any).show !== false);
+9 -13
View File
@@ -29,7 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Row 1: standard header — title + actions only (Import/Add). The alert
type toggle, search and folder scope moved into the table toolbar. -->
<template #header>
<AppPageHeader :title="t('alerts.header')" :subtitle="'Alert rules and notification channels'" icon="shield-alert-outline">
<AppPageHeader :title="t('alerts.header')" :subtitle="t('alerts.subtitle')" icon="shield-alert-outline">
<template #actions>
<!-- Import button -->
<OButton
@@ -155,14 +155,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="folder-outline"
data-test="alert-list-search-scope-current"
title="Search only this folder"
>This folder</OToggleGroupItem>
>{{ t('alerts.searchThisFolder') }}</OToggleGroupItem>
<OToggleGroupItem
value="all"
size="xs"
icon-left="search"
data-test="alert-list-search-across-folders-toggle"
title="Search across all folders"
>All folders</OToggleGroupItem>
>{{ t('alerts.searchAllFolders') }}</OToggleGroupItem>
</OToggleGroup>
</template>
</OInput>
@@ -421,9 +421,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="tw:text-sm"
data-test="alert-list-create-template-text"
>
It looks like you haven't created any Templates yet.
To create an Alert, you'll need to have at least one
Destination and one Template in place
{{ t('alerts.noTemplatesMsg') }}
</div>
<OButton
data-test="alert-list-create-template-btn"
@@ -431,16 +429,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="primary"
size="sm"
@click="routeTo('alertTemplates')"
>Create Template</OButton>
>{{ t('alerts.createTemplateBtn') }}</OButton>
</template>
<template v-if="!destinations.length && templates.length">
<div
class="tw:text-sm"
data-test="alert-list-create-destination-text"
>
It looks like you haven't created any Destinations
yet. To create an Alert, you'll need to have at least
one Destination and one Template in place
{{ t('alerts.noDestinationsMsg') }}
</div>
<OButton
data-test="alert-list-create-destination-btn"
@@ -448,7 +444,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="primary"
size="sm"
@click="routeTo('alertDestinations')"
>Create Destination</OButton>
>{{ t('alerts.createDestinationBtn') }}</OButton>
</template>
</div>
</div>
@@ -1077,7 +1073,7 @@ export default defineComponent({
{
id: "last_trained_at",
accessorKey: "last_trained_at",
header: "Last Trained At",
header: t("alerts.lastTrainedAt"),
cell: " ",
sortable: true,
resizable: true,
@@ -1088,7 +1084,7 @@ export default defineComponent({
{
id: "status",
accessorKey: "status",
header: "Status",
header: t("alerts.status"),
cell: " ",
sortable: true,
resizable: true,
+1 -1
View File
@@ -29,7 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click="addApiHeader"
icon-left="add"
>
Add Condition
{{ t('alerts.addCondition') }}
</OButton>
</template>
<template v-else>
+2 -2
View File
@@ -25,7 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<AppPageHeader
:title="t('alerts.incidents.title')"
icon="notifications-active"
:subtitle="'Incident management and tracking'"
:subtitle="t('alerts.incidents.subtitle')"
>
<template #actions>
<OButton
@@ -34,7 +34,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:loading="loading"
@click="refreshIncidents"
data-test="incident-refresh-btn"
>Refresh</OButton>
>{{ t('common.refresh') }}</OButton>
</template>
</AppPageHeader>
</template>
+16 -16
View File
@@ -69,7 +69,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template v-if="isEventBased">
<!-- Alert if row -->
<div class="alert-condition-row" data-test="alert-if-row-logs">
<span class="condition-label">Alert if *</span>
<span class="condition-label">{{ t('alerts.threshold') }}*</span>
<div class="tw:flex tw:flex-nowrap tw:items-center tw:gap-2">
<div class="tw:min-w-[130px] tw:max-w-[180px]">
<OSelect
@@ -83,7 +83,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<!-- "of [field]" shown for measure modes -->
<template v-if="selectedFunction !== 'total_events'">
<span class="condition-text">of</span>
<span class="condition-text">{{ t('alerts.conditionOf') }}</span>
<OSelect
v-model="logMeasureColumn"
:options="numericColumns"
@@ -115,12 +115,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:error-message="triggerThresholdError"
@update:model-value="triggerThresholdError = ''; onTriggerThresholdChange($event)"
/>
<span v-if="streamName" class="condition-text">matching {{ streamType === 'traces' ? 'traces' : 'logs' }} found</span>
<span v-if="streamName" class="condition-text">{{ t('alerts.matchingTypeFound', { type: streamType === 'traces' ? 'traces' : 'logs' }) }}</span>
</template>
<!-- MEASURE mode -->
<template v-else>
<span class="condition-text">is</span>
<span class="condition-text">{{ t('alerts.conditionIs') }}</span>
<OSelect
v-model="conditionOperator"
:options="numericOperators"
@@ -216,7 +216,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template v-else>
<!-- Alert if row -->
<div class="alert-condition-row">
<span class="condition-label">Alert if *</span>
<span class="condition-label">{{ t('alerts.threshold') }}*</span>
<div class="tw:flex tw:flex-nowrap tw:items-center tw:gap-2">
<div class="tw:min-w-[130px] tw:max-w-[180px]">
<OSelect
@@ -232,7 +232,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- "of [field]" hidden for count mode -->
<template v-if="selectedFunction !== 'total_events'">
<span class="condition-text">of</span>
<span class="condition-text">{{ t('alerts.conditionOf') }}</span>
<div style="position: relative; display: inline-flex;">
<OSelect
v-model="inputData.aggregation.having.column"
@@ -246,9 +246,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:class="columnSelectError ? 'column-select-error' : ''"
style="min-width: 140px; max-width: 200px;"
/>
<OTooltip v-if="inputData.aggregation.having.column === 'value' && columns.some((c: any) => (typeof c === 'string' ? c : c.value) === 'value')" content="Metrics streams store their measurement in the &quot;value&quot; field by default" :delay="300" side="bottom" />
<OTooltip v-if="inputData.aggregation.having.column === 'value' && columns.some((c: any) => (typeof c === 'string' ? c : c.value) === 'value')" :content="t('alerts.metricsValueFieldTooltip')" :delay="300" side="bottom" />
</div>
<span class="condition-text">is</span>
<span class="condition-text">{{ t('alerts.conditionIs') }}</span>
</template>
<!-- Count mode for metrics -->
@@ -270,7 +270,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:error-message="triggerThresholdError"
@update:model-value="triggerThresholdError = ''; onTriggerThresholdChange($event)"
/>
<span class="condition-text">matching metrics found</span>
<span class="condition-text">{{ t('alerts.matchingMetricsFound') }}</span>
</template>
<!-- Measure mode for metrics -->
@@ -369,7 +369,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="alert-condition-row tw:!tw:items-start">
<span class="condition-label" style="line-height: 28px;">
Check every *
<OTooltip content="How often to check this alert condition" :delay="300" side="top" />
<OTooltip :content="t('alerts.howOftenCheckTooltip')" :delay="300" side="top" />
</span>
<div class="tw:flex tw:flex-col tw:gap-1">
<div class="tw:flex tw:items-center tw:gap-2">
@@ -568,7 +568,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="inline-editor-header tw:flex tw:items-center tw:justify-between">
<div class="tw:flex tw:items-center tw:gap-2">
<div class="pane-accent-bar pane-accent-bar--primary" />
<span class="inline-editor-title">{{ localTab === 'sql' ? 'SQL Editor' : 'PromQL Editor' }}</span>
<span class="inline-editor-title">{{ localTab === 'sql' ? t('alerts.sqlEditor') : t('alerts.promqlEditor') }}</span>
</div>
<!-- fx toggle shown here only when VRL is not yet enabled -->
<OSwitch
@@ -691,7 +691,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="alert-condition-row tw:!tw:items-start">
<span class="condition-label sql-promql-label" style="line-height: 28px;">
Check every *
<OTooltip content="How often to check this alert condition" :delay="300" side="top" />
<OTooltip :content="t('alerts.howOftenCheckTooltip')" :delay="300" side="top" />
</span>
<div class="tw:flex tw:flex-col tw:gap-1">
<div class="tw:flex tw:items-center tw:gap-2">
@@ -758,7 +758,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- SQL: Alert if No. of events -->
<div v-if="localTab === 'sql'" class="alert-condition-row">
<span class="condition-label sql-promql-label">Alert if No. of events *</span>
<span class="condition-label sql-promql-label">{{ t('alerts.alertIfNoOfEvents') }} *</span>
<div class="tw:flex tw:items-center tw:gap-2">
<OSelect
v-model="triggerOperator"
@@ -786,7 +786,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- PromQL: Alert if the value is + Having series -->
<template v-if="localTab === 'promql' && promqlCondition">
<div class="alert-condition-row">
<span class="condition-label sql-promql-label">Alert if the value is *
<span class="condition-label sql-promql-label">{{ t('alerts.alertIfValueIs') }} *
<OTooltip content="Alert when the PromQL expression evaluates to this condition for a time series. Example: &gt;= 100 triggers when the result is 100 or more." :delay="300" side="top" />
</span>
<div class="tw:flex tw:items-center tw:gap-2">
@@ -815,7 +815,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<div class="alert-condition-row">
<span class="condition-label sql-promql-label">Having series *
<span class="condition-label sql-promql-label">{{ t('alerts.havingSeries') }} *
<OTooltip content="Minimum number of time series that must satisfy the condition above to trigger the alert." :delay="300" side="top" />
</span>
<div class="tw:flex tw:items-center tw:gap-2">
@@ -871,7 +871,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Multi-Window Confirmation Dialog -->
<CustomConfirmDialog
v-model="showMultiWindowDialog"
title="Clear Multi-Windows?"
:title="t('alerts.clearMultiWindowsTitle')"
:message="`Multi-windows are configured. To enable ${pendingTab === 'custom' ? 'Custom' : 'PromQL'} mode, we need to clear them. Do you want to proceed?`"
@confirm="handleConfirmClearMultiWindows"
@cancel="handleCancelClearMultiWindows"
@@ -57,7 +57,7 @@
@click="addNewDrilldown"
data-test="dashboard-addpanel-config-drilldown-add-btn"
>
+ Add
+ {{ t('common.add') }}
</OButton>
<DrilldownPopUp
:open="showDrilldownPopUp"
@@ -23,6 +23,8 @@ vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: vi.fn((key: string) => {
const translations: Record<string, string> = {
"dashboard.newVariable": "Add Variable",
"dashboard.editVariable": "Edit Variable",
"dashboard.typeOfVariable": "Type of Variable",
"dashboard.nameOfVariable": "Name of Variable",
"dashboard.labelOfVariable": "Label of Variable",
@@ -30,10 +30,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="tw:mt-3">
<div class="tw:mb-3">
<OSelect
helpText="Variables will be applied to all tabs and panels if global is selected."
:helpText="t('dashboard.variableScopeHelp')"
v-model="variableData.scope"
:options="scopeOptions"
label="Select variable scope"
:label="t('dashboard.selectVariableScope')"
data-test="dashboard-variable-scope-select"
/>
</div>
@@ -160,8 +160,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #tooltip>
<OTooltip max-width="250px">
<template #content>
Select a stream or use a variable like $streamVariable
to dynamically choose the stream based on another value.
{{ t('dashboard.streamSelectTooltip') }}
</template>
</OTooltip>
</template>
@@ -184,9 +183,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #tooltip>
<OTooltip max-width="250px">
<template #content>
Select a field or use a variable like $fieldVariable. If
stream uses a variable, field list will be empty - type
field name manually.
{{ t('dashboard.fieldSelectTooltip') }}
</template>
</OTooltip>
</template>
@@ -220,9 +217,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="tw:cursor-help"
/>
<template #content>
In filters, you can use the value of another variable to
filter the current variable's value. This can be done by
using the other variable's name. For example:
{{ t('dashboard.filterInfoTooltip') }}
<span class="bg-highlight">$variableName</span>.
</template>
</OTooltip>
@@ -252,7 +247,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<template #empty>
<span class="tw:italic tw:text-gray-400"
>No Data Found</span
>{{ t('dashboard.noDataFound') }}</span
>
</template>
</OSelect>
@@ -318,7 +313,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-add-filter-btn"
icon-left="add"
>
Add Filter
{{ t('dashboard.addFilter') }}
</OButton>
</div>
@@ -350,10 +345,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="tw:flex">
<div class="tw:w-6"></div>
<div class="tw:flex-1 tw:font-semibold tw:text-gray-500">
Label
{{ t('common.label') }}
</div>
<div class="tw:flex-1 tw:font-semibold tw:text-gray-500">
Value
{{ t('common.value') }}
</div>
<div class="tw:w-12 tw:flex tw:items-center tw:justify-center">
<span v-if="!variableData.multiSelect"> Default </span>
@@ -364,7 +359,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click="onCustomSelectAllClick"
>
<template #tooltip
><OTooltip content="Default - Select All"
><OTooltip :content="t('dashboard.defaultSelectAll')"
/></template>
</OCheckbox>
</div>
@@ -419,7 +414,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-add-option-btn"
icon-left="add"
>
Add Option
{{ t('dashboard.addOption') }}
</OButton>
</div>
</div>
@@ -439,7 +434,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- it can be first value or all values -->
<div v-if="['query_values'].includes(variableData.type)">
<div class="button-group multi-select-default-value-toggle">
<div class="multi-select-default-value">By Default Select:</div>
<div class="multi-select-default-value">{{ t('dashboard.byDefaultSelect') }}</div>
<OToggleGroup
:model-value="variableData.selectAllValueForMultiSelect"
@update:model-value="
@@ -450,19 +445,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
value="first"
size="sm"
data-test="dashboard-multi-select-default-value-toggle-first-value"
>First value</OToggleGroupItem
>{{ t('dashboard.firstValue') }}</OToggleGroupItem
>
<OToggleGroupItem
value="all"
size="sm"
data-test="dashboard-multi-select-default-value-toggle-all-values"
>All values</OToggleGroupItem
>{{ t('dashboard.allValues') }}</OToggleGroupItem
>
<OToggleGroupItem
value="custom"
size="sm"
data-test="dashboard-multi-select-default-value-toggle-custom"
>Custom</OToggleGroupItem
>{{ t('dashboard.customValue') }}</OToggleGroupItem
>
</OToggleGroup>
</div>
@@ -537,9 +532,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #tooltip>
<OTooltip max-width="300px">
<template #content>
If enabled, single quotes will be escaped in the query. For
example, a value like `O'Reilly` will be replaced as
`O''Reilly`.
{{ t('dashboard.escapeSingleQuotesTooltip') }}
</template>
</OTooltip>
</template>
@@ -561,7 +554,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:loading="saveVariableApiCall.isLoading.value"
@click="addVariableForm?.submit()"
data-test="dashboard-variable-save-btn"
>Save</OButton
>{{ t("dashboard.save") }}</OButton
>
</div>
</div>
@@ -730,7 +723,7 @@ export default defineComponent({
selectedStreamFields: [],
});
const route = useRoute();
const title = ref("Add Variable");
const title = ref(t("dashboard.newVariable"));
const { getStreams, getStream } = useStreams();
const {
showErrorNotification,
@@ -887,7 +880,7 @@ export default defineComponent({
if (props.variableName) {
editMode.value = true;
title.value = "Edit Variable";
title.value = t("dashboard.editVariable");
const variablesList = data.variables?.list || [];
const variable = variablesList.find(
@@ -35,7 +35,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="dateTimeValue"
data-test="dashboard-general-setting-datetime-picker"
>
<label>Default Duration</label>
<label>{{ t('dashboard.defaultDuration') }}</label>
<DateTimePickerDashboard
v-show="store.state.printMode === false"
ref="dateTimePicker"
@@ -49,7 +49,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<OFormSwitch
name="showDynamicFilters"
label="Show Dynamic Filters"
:label="t('dashboard.showDynamicFilters')"
data-test="dashboard-general-setting-dynamic-filter"
size="lg"
/>
@@ -35,7 +35,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="sm"
@click="showVariablesDependenciesGraphPopUp = true"
data-test="dashboard-variable-dependencies-btn"
>Show Dependencies</OButton
>{{ t('dashboard.showDependencies') }}</OButton
>
<OButton
variant="primary"
@@ -129,7 +129,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="getScopeType(variable) === 'tabs' && variable.tabs?.length"
>
<template #content>
<div>Applied to tabs:</div>
<div>{{ t('dashboard.appliedToTabs') }}</div>
<div v-for="tabId in variable.tabs" :key="tabId">{{ getTabName(tabId) }}</div>
</template>
</OTooltip>
@@ -138,7 +138,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="getScopeType(variable) === 'panels' && variable.panels?.length"
>
<template #content>
<div>Applied to panels:</div>
<div>{{ t('dashboard.appliedToPanels') }}</div>
<div v-for="panelId in variable.panels" :key="panelId">{{ getPanelName(panelId) }}</div>
</template>
</OTooltip>
@@ -173,8 +173,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</draggable>
<ConfirmDialog
title="Delete Variable"
message="Are you sure you want to delete the variable?"
:title="t('dashboard.deleteVariable')"
:message="t('dashboard.deleteVariableMsg')"
@update:ok="deleteVariableFn"
@update:cancel="confirmDeleteDialog = false"
v-model="confirmDeleteDialog"
@@ -27,7 +27,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<AppPageHeader
:title="t('function.enrichmentTables')"
icon="dataset"
:subtitle="'Lookup tables that enrich ingested data'"
:subtitle="t('function.enrichmentTablesSubtitle')"
tabs-below
class="tw:shrink-0 tw:px-4"
>
@@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<AppPageHeader
:title="t('function.header')"
icon="function"
:subtitle="'Reusable VRL functions applied in pipelines'"
:subtitle="t('function.subtitle')"
tabs-below
class="tw:shrink-0 tw:px-4"
>
@@ -4,6 +4,7 @@ import TestFunction from './TestFunction.vue';
import { createStore } from 'vuex';
import { createI18n } from 'vue-i18n';
import { nextTick } from 'vue';
import enLocale from '@/locales/languages/en.json';
// Mock dependencies
vi.mock('@/composables/useStreams', () => ({
@@ -64,27 +65,13 @@ const mockStore = createStore({
},
});
// Use the real en.json so translation keys resolve exactly as they do in the app.
// This prevents the spec's i18n dictionary from drifting out of sync with the
// component's t() keys (the cause of the earlier "function.clickTestFunctionHint" failures).
const mockI18n = createI18n({
locale: 'en',
messages: {
en: {
common: {
query: 'Query',
events: 'Events',
output: 'Output',
duration: 'Duration',
},
alerts: {
streamType: 'Stream Type',
stream_name: 'Stream Name',
},
search: {
runQuery: 'Run Query',
},
confirmDialog: {
loading: 'Loading',
},
},
en: enLocale,
},
});
@@ -490,7 +490,7 @@ const setEventsEditor = () => {
const outputMessage = computed(() => {
if (!outputEvents.value) {
return "Please click Test Function to see the events";
return t("function.clickTestFunctionHint");
}
return "";
@@ -500,9 +500,9 @@ const areInputValid = () => {
if (!inputQuery.value) {
toast({
variant: "error",
message: "Please enter a query",
message: t("function.pleaseEnterQuery"),
});
sqlQueryErrorMsg.value = "Please enter a query";
sqlQueryErrorMsg.value = t("function.pleaseEnterQuery");
return false;
}
+2 -2
View File
@@ -34,7 +34,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="add-group-groupname-input-btn"
:error="showNameError"
:error-message="nameErrorMessage"
:help-text="!showNameError ? `Use alphanumeric and '_' characters only, without spaces.` : undefined"
:help-text="!showNameError ? t('iam.nameHelpText') : undefined"
@update:model-value="showNameError = !!name && !isValidGroupName"
/>
</div>
@@ -93,7 +93,7 @@ watch(
}
);
const nameErrorMessage = computed(() =>
!name.value ? t('common.nameRequired') : `Use alphanumeric and '_' characters only, without spaces.`
!name.value ? t('common.nameRequired') : t('iam.nameHelpText')
);
const saveGroup = () => {
@@ -43,7 +43,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="showLabelOnTop tw:mt-2"
:error="showNameError"
:error-message="nameErrorMessage"
:help-text="!showNameError ? 'Use alphanumeric characters, space and underscore only.' : undefined"
:help-text="!showNameError ? t('organization.nameHelpText') : undefined"
@update:model-value="showNameError = !!organizationData.name && !isValidOrgName"
data-test="org-name"
maxlength="100"
@@ -136,7 +136,7 @@ export default defineComponent({
const nameErrorMessage = computed(() =>
!organizationData.value.name
? t('organization.nameRequired')
: `Use alphanumeric characters, space and underscore only.`
: t('organization.nameHelpText')
);
watch(
+2 -2
View File
@@ -34,7 +34,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="add-role-rolename-input-btn"
:error="showNameError"
:error-message="nameErrorMessage"
:help-text="!showNameError ? `Use alphanumeric and '_' characters only, without spaces.` : undefined"
:help-text="!showNameError ? t('iam.nameHelpText') : undefined"
@update:model-value="showNameError = !!name && !isValidRoleName"
/>
</div>
@@ -94,7 +94,7 @@ watch(
);
const nameErrorMessage = computed(() =>
!name.value ? t('common.nameRequired') : `Use alphanumeric and '_' characters only, without spaces.`
!name.value ? t('common.nameRequired') : t('iam.nameHelpText')
);
const saveRole = () => {
+5 -39
View File
@@ -26,6 +26,7 @@ import {
import { mount, flushPromises } from "@vue/test-utils";
import User from "@/components/iam/users/User.vue";
import { createI18n } from "vue-i18n";
import enLocale from "@/locales/languages/en.json";
import store from "@/test/unit/helpers/store";
import router from "@/test/unit/helpers/router";
import usersService from "@/services/users";
@@ -42,46 +43,11 @@ const i18n = createI18n({
globalInjection: true,
silentTranslationWarn: true,
silentFallbackWarn: true,
// Use the real en.json so dialog titles/labels resolve exactly as in the app.
// Dialog titles (e.g. "Delete Users") are now translated via t("user.deleteUsersTitle");
// sourcing the real dictionary keeps the title-based dialog lookups in this spec working.
messages: {
en: {
user: {
email: "Email",
firstName: "First Name",
lastName: "Last Name",
role: "Role",
actions: "Actions",
name: "Name",
password: "Password",
confirmPassword: "Confirm Password",
search: "Search",
add: "Add User",
update: "Update User",
editUser: "Edit User",
cancel: "Cancel",
save: "Save",
confirmDeleteHead: "Delete User",
confirmDeleteMsg: "Are you sure you want to delete this user?",
ok: "OK",
header: "User",
delete: "Delete",
revoke_invite: "Revoke Invitation",
},
iam: { basicUsers: "Users" },
search: {
showing: "Showing",
of: "of",
recordsPerPage: "Records Per Page",
},
ticket: { noDataErrorMsg: "No data available" },
common: {
cancel: "Cancel",
save: "Save",
delete: "Delete",
edit: "Edit",
add: "Add",
update: "Update",
},
},
en: enLocale,
},
});
+9 -9
View File
@@ -22,7 +22,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
the table's own toolbar (built-in global filter) per the layout system. -->
<AppPageHeader
:title="t('iam.basicUsers')"
:subtitle="'People with access to this organization'"
:subtitle="t('user.subtitle')"
icon="person"
class="tw:shrink-0 tw:px-4 tw:border-b tw:border-border-default"
>
@@ -203,25 +203,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<ODialog data-test="user-revoke-dialog"
v-model:open="confirmRevoke"
size="xs"
title="Revoke Invitation"
:title="t('user.revokeInvitationTitle')"
:secondary-button-label="t('user.cancel')"
:primary-button-label="t('user.ok')"
@click:secondary="confirmRevoke = false"
@click:primary="revokeInvite"
>
<p>Are you sure you want to revoke the invitation for {{ revokeInviteEmail }}?</p>
<p>{{ t('user.revokeInvitationMsg', { email: revokeInviteEmail }) }}</p>
</ODialog>
<ODialog data-test="user-bulk-delete-dialog"
v-model:open="confirmBulkDelete"
size="sm"
title="Delete Users"
secondary-button-label="Cancel"
primary-button-label="OK"
:title="t('user.deleteUsersTitle')"
:secondary-button-label="t('user.cancel')"
:primary-button-label="t('user.ok')"
@click:secondary="confirmBulkDelete = false"
@click:primary="bulkDeleteUsers"
>
<p>Are you sure you want to delete {{ selectedUsers.length }} user(s)?</p>
<p>{{ t('user.deleteUsersMsg', { count: selectedUsers.length }) }}</p>
</ODialog>
</div>
</template>
@@ -413,7 +413,7 @@ export default defineComponent({
if (isEnterpriseOrCloud) {
cols.push({
id: "auth",
header: "Auth",
header: t("user.authType"),
accessorKey: "auth_type",
sortable: true,
resizable: true,
@@ -426,7 +426,7 @@ export default defineComponent({
// Roles column — array of role chips in enterprise/cloud, single role string otherwise
cols.push({
id: isEnterpriseOrCloud ? "roles" : "role",
header: isEnterpriseOrCloud ? "Roles" : t("user.role"),
header: isEnterpriseOrCloud ? t("user.roles") : t("user.role"),
accessorKey: isEnterpriseOrCloud ? "roles" : "role",
sortable: true,
resizable: true,
+1 -1
View File
@@ -198,7 +198,7 @@ const dataRetentionError = ref('');
// Allowed characters mirror the backend `format_stream_name` regex
// (src/config/src/utils/schema.rs): alphanumeric, underscore and colon only.
const streamNameRegex = /^[a-zA-Z0-9_:]+$/;
const streamNameHelpText = "Use alphanumeric characters, underscore and colon only.";
const streamNameHelpText = t("logStream.streamNameHelpText");
const validateStreamName = () => {
if (streamInputs.value.name && !streamNameRegex.test(streamInputs.value.name)) {
@@ -17,7 +17,7 @@
data-test="associated-regex-patterns-title-text"
>
<span class="breadcrumb-text tw:cursor-pointer" @click="closeDialog"
>Stream Details &gt; &nbsp;
>{{ t('regex_patterns.associated_breadcrumb') }} &gt; &nbsp;
</span>
<span class="associated-field-name">
{{ fieldName }}
@@ -46,7 +46,7 @@
data-test="associated-regex-patterns-search-input"
v-model="filterPattern"
data-cy="schema-index-field-search-input"
placeholder="Search"
:placeholder="t('regex_patterns.associated_search_placeholder')"
clearable
/>
</div>
@@ -54,7 +54,7 @@
<div class="pattern-list-wrapper">
<OCollapsible
v-model="appliedPatternsExpandedRef"
:label="`Applied Patterns (${appliedPatterns.length})`"
:label="t('regex_patterns.applied_patterns_count', { count: appliedPatterns.length })"
class="tw:mt-2 tw:text-[14px] tw:font-[600]"
data-test="associated-regex-patterns-applied-patterns-expansion-item"
>
@@ -63,7 +63,7 @@
class="tw:py-3 tw:px-2 tw:text-[12px] tw:opacity-50"
data-test="associated-regex-patterns-applied-patterns-table"
>
No data available
{{ t('regex_patterns.no_data_available') }}
</div>
<ul v-else class="tw:list-none tw:p-0 tw:m-0" data-test="associated-regex-patterns-applied-patterns-table">
<li
@@ -84,7 +84,7 @@
<div class="pattern-list-wrapper">
<OCollapsible
v-model="allPatternsExpandedRef"
:label="`All Patterns (${resultTotal})`"
:label="t('regex_patterns.all_patterns_count', { count: resultTotal })"
class="tw:mt-2 tw:text-[14px] tw:font-[600]"
data-test="associated-regex-patterns-all-patterns-expansion-item"
>
@@ -123,13 +123,12 @@
<span
class="no-pattern-applied-title"
data-test="associated-regex-patterns-no-pattern-applied-title"
>No Patterns Applied Yet</span
>{{ t('regex_patterns.no_patterns_applied_title') }}</span
>
<span
class="no-pattern-applied-subtitle tw:text-center"
data-test="associated-regex-patterns-no-pattern-applied-subtitle"
>Browse the pattern library to begin applying regular expressions
to your fields.</span
>{{ t('regex_patterns.no_patterns_applied_subtitle') }}</span
>
</div>
<div
@@ -148,7 +147,7 @@
<div class="tw:flex tw:flex-col tw:gap-2">
<div class="tw:flex tw:flex-col tw:gap-1">
<span class="individual-section-title tw:text-[12px] tw:font-[500]">
Pattern Name
{{ t('regex_patterns.pattern_name') }}
</span>
<span
class="individual-section-value tw:text-[15px] tw:font-[700]"
@@ -162,7 +161,7 @@
<div class="tw:flex tw:flex-col tw:gap-1">
<span class="individual-section-title tw:text-[12px] tw:font-[500]">
Description
{{ t('regex_patterns.description') }}
</span>
<span
class="individual-section-value tw:text-[15px] tw:font-[700]"
@@ -171,7 +170,7 @@
{{
userClickedPattern.description
? userClickedPattern.description
: "No description available"
: t('regex_patterns.no_description')
}}
</span>
</div>
@@ -191,7 +190,7 @@
<!-- when value matches -->
<div class="tw:flex tw:flex-col tw:gap-1.5 tw:flex-1">
<span class="individual-section-title tw:text-[12px] tw:font-[500]">
When value matches
{{ t('regex_patterns.when_value_matches') }}
</span>
<ORadioGroup v-model="policy">
<div class="tw:flex tw:flex-col tw:gap-1">
@@ -201,8 +200,8 @@
data-test="associated-regex-patterns-redact-radio"
size="sm"
/>
<span class="tw:font-[600] tw:text-[13px]">Redact</span>
<span class="tw:font-[400] tw:text-[12px] tw:opacity-60">Replace with [REDACTED]</span>
<span class="tw:font-[600] tw:text-[13px]">{{ t('regex_patterns.redact') }}</span>
<span class="tw:font-[400] tw:text-[12px] tw:opacity-60">{{ t('regex_patterns.redact_hint') }}</span>
</div>
<div class="tw:flex tw:items-center tw:gap-2">
<ORadio
@@ -210,8 +209,8 @@
data-test="associated-regex-patterns-drop-field-radio"
size="sm"
/>
<span class="tw:font-[600] tw:text-[13px]">Drop</span>
<span class="tw:font-[400] tw:text-[12px] tw:opacity-60">Drop the field completely</span>
<span class="tw:font-[600] tw:text-[13px]">{{ t('regex_patterns.drop') }}</span>
<span class="tw:font-[400] tw:text-[12px] tw:opacity-60">{{ t('regex_patterns.drop_hint') }}</span>
</div>
<div class="tw:flex tw:items-center tw:gap-2">
<ORadio
@@ -219,8 +218,8 @@
data-test="associated-regex-patterns-hash-radio"
size="sm"
/>
<span class="tw:font-[600] tw:text-[13px]">Hash</span>
<span class="tw:font-[400] tw:text-[12px] tw:opacity-60">Replace with searchable hash</span>
<span class="tw:font-[600] tw:text-[13px]">{{ t('regex_patterns.hash') }}</span>
<span class="tw:font-[400] tw:text-[12px] tw:opacity-60">{{ t('regex_patterns.hash_hint') }}</span>
</div>
</div>
</ORadioGroup>
@@ -231,21 +230,21 @@
<!-- detect at section -->
<div class="tw:flex tw:flex-col tw:gap-1.5 tw:min-w-[120px]">
<span class="individual-section-title tw:text-[12px] tw:font-[500]">
Detect at
{{ t('regex_patterns.detect_at') }}
</span>
<div class="tw:flex tw:flex-col tw:gap-1.5">
<OCheckbox
size="sm"
v-model="apply_at"
val="AtIngestion"
label="Ingestion"
:label="t('regex_patterns.ingestion')"
data-test="associated-regex-patterns-ingestion-checkbox"
/>
<OCheckbox
size="sm"
v-model="apply_at"
val="AtSearch"
label="Query"
:label="t('regex_patterns.query')"
data-test="associated-regex-patterns-query-checkbox"
/>
</div>
@@ -269,7 +268,7 @@
<span
class="individual-section-title-main tw:text-[13px] tw:font-[700]"
>
Test Pattern
{{ t('regex_patterns.test_pattern') }}
</span>
<OButton
:disabled="testString.length === 0 || testLoading"
@@ -277,7 +276,7 @@
size="sm-action"
@click="testStringOutput"
>
<span class="tw:text-[12px]">Test Input</span>
<span class="tw:text-[12px]">{{ t('regex_patterns.test_input') }}</span>
</OButton>
</div>
@@ -285,7 +284,7 @@
<span
class="individual-section-sub-title2 tw:text-[12px] tw:font-[500]"
>
Regex Pattern
{{ t('regex_patterns.regex_pattern_label') }}
</span>
<div
class="tw:p-2 tw:rounded tw:font-mono tw:text-[11px] tw:break-all"
@@ -311,7 +310,7 @@
<FullViewContainer
name="query"
v-model:is-expanded="expandState.regexTestString"
label="Input string"
:label="t('regex_patterns.input_string')"
class="tw:py-md tw:h-[24px]"
:labelClass="
store.state.theme === 'dark'
@@ -333,7 +332,7 @@
: 'light-mode-regex-test-string-input'
"
type="textarea"
placeholder="Eg. 1234567890"
:placeholder="t('regex_patterns.input_string_placeholder')"
:rows="5"
style="width: 100%"
/>
@@ -344,7 +343,7 @@
<FullViewContainer
name="output"
v-model:is-expanded="expandState.outputString"
label="Output"
:label="t('regex_patterns.output')"
class="tw:py-md tw:h-[24px]"
:labelClass="
store.state.theme === 'dark'
@@ -367,7 +366,7 @@
: 'light-mode-regex-test-string-input'
"
type="textarea"
placeholder="Output String"
:placeholder="t('regex_patterns.output_string_placeholder')"
:rows="5"
style="width: 100%"
/>
@@ -398,7 +397,7 @@
: 'tw:text-[#4B5563]'
"
>
Please click Test Input to see the results
{{ t('regex_patterns.click_test_input_hint') }}
</span>
</div>
<div v-else-if="testLoading">
@@ -430,8 +429,8 @@
>
{{
checkIfPatternIsApplied(userClickedPattern.pattern_id)
? "Remove Pattern"
: "Add Pattern"
? t('regex_patterns.remove_pattern')
: t('regex_patterns.add_pattern')
}}
</OButton>
</div>
@@ -442,8 +441,8 @@
</div>
</div>
<ConfirmDialog
title="Remove Pattern"
message="Are you sure you want to remove this pattern from the field?"
:title="t('regex_patterns.remove_pattern')"
:message="t('regex_patterns.remove_pattern_confirm_msg')"
@update:ok="handleAddOrRemovePattern"
@update:cancel="handleCancelRemovePattern"
v-model="showWarningDialogToRemovePattern"
@@ -591,7 +590,7 @@ export default defineComponent({
} catch (error) {
toast({
variant: "error",
message: error.response?.data?.message || "Failed to test string",
message: error.response?.data?.message || t("regex_patterns.failed_to_test_string"),
});
} finally {
testLoading.value = false;
@@ -792,7 +791,7 @@ export default defineComponent({
if (apply_at.value.length == 0) {
toast({
variant: "error",
message: "Please select detect at option",
message: t("regex_patterns.select_detect_at_option"),
});
return;
}
@@ -169,7 +169,7 @@ const fieldDataTypeErrors = ref<string[]>([]);
// Allowed characters mirror the backend `format_stream_name` regex
// (src/config/src/utils/schema.rs): alphanumeric, underscore and colon only.
const fieldNameRegex = /^[a-zA-Z0-9_:]+$/;
const fieldNameHelpText = "Use alphanumeric characters, underscore and colon only.";
const fieldNameHelpText = t("logStream.streamNameHelpText");
const validateFieldName = (index: number) => {
const field = (props.fields as any[])[index];
+9 -9
View File
@@ -23,7 +23,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Row 1: standard header — title + actions only. Tabs / search / folder
scope moved into the table's own toolbar below. -->
<template #header>
<AppPageHeader icon="description" :subtitle="'Scheduled reports and exports'">
<AppPageHeader icon="description" :subtitle="t('reports.subtitle')">
<template #title><span data-test="report-list-title">{{ t('reports.header') }}</span></template>
<template #actions>
<OButton
@@ -109,14 +109,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="folder-outline"
data-test="report-list-search-scope-current"
title="Search only this folder"
>This folder</OToggleGroupItem>
>{{ t('reports.searchThisFolder') }}</OToggleGroupItem>
<OToggleGroupItem
value="all"
size="xs"
icon-left="search"
data-test="report-list-search-across-folders-toggle"
title="Search across all folders"
>All folders</OToggleGroupItem>
>{{ t('reports.searchAllFolders') }}</OToggleGroupItem>
</OToggleGroup>
</template>
</OInput>
@@ -129,7 +129,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="icon-sm"
icon-left="refresh"
:loading="isLoadingReports"
title="Reload reports"
:title="t('reports.reloadReports')"
data-test="report-list-refresh-btn"
@click="() => { invalidateFolderCache(activeFolderId); loadReports(activeFolderId); }"
/>
@@ -209,7 +209,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
icon-left="drive-file-move"
variant="ghost"
size="icon-sm"
title="Move to Folder"
:title="t('reports.moveToFolder')"
@click="openMoveDialog(row)"
/>
@@ -240,7 +240,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="sm-action"
@click="moveMultipleReports"
>
Move
{{ t('common.move') }}
</OButton>
<OButton
v-if="selectedReports.length > 0"
@@ -250,7 +250,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="sm-action"
@click="openBulkDeleteDialog"
>
Delete
{{ t('common.delete') }}
</OButton>
</div>
</div>
@@ -273,8 +273,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Bulk delete confirm -->
<ConfirmDialog
v-model="confirmBulkDelete"
title="Delete Reports"
:message="`Are you sure you want to delete ${selectedReports.length} report(s)?`"
:title="t('reports.deleteReportsTitle')"
:message="t('reports.deleteReportsMsg', { count: selectedReports.length })"
@update:ok="bulkDeleteReports"
@update:cancel="confirmBulkDelete = false"
/>
+27 -17
View File
@@ -213,7 +213,7 @@ export default defineComponent({
{
key: "general",
label: t("settings.generalLabel"),
description: "Theme, query defaults, and core preferences",
description: t("settings.generalDesc"),
icon: "settings",
to: `/settings/general?org_identifier=${org}`,
dataTest: "general-settings-tab",
@@ -222,7 +222,7 @@ export default defineComponent({
{
key: "organization",
label: t("settings.orgLabel"),
description: "Organization parameters and identifiers",
description: t("settings.orgDesc"),
icon: "business",
to: `/settings/organization?org_identifier=${org}`,
dataTest: "organization-settings-tab",
@@ -231,7 +231,7 @@ export default defineComponent({
{
key: "cipher-keys",
label: t("settings.cipherKeys"),
description: "Encryption keys for sensitive fields",
description: t("settings.cipherKeysDesc"),
icon: "key",
to: { name: "cipherKeys", query: { org_identifier: org } },
visible: isEnt,
@@ -241,7 +241,7 @@ export default defineComponent({
{
key: "regex_patterns",
label: t("regex_patterns.title"),
description: "Reusable regex patterns for redaction",
description: t("settings.regexPatternsDesc"),
icon: `img:${regexIcon.value}`,
to: { name: "regexPatterns", query: { org_identifier: org } },
visible: isEnt,
@@ -251,7 +251,7 @@ export default defineComponent({
{
key: "domain_management",
label: t("settings.ssoDomainRestrictions"),
description: "Restrict sign-in to allowed SSO domains",
description: t("settings.domainManagementDesc"),
icon: "dns",
to: { name: "domainManagement", query: { org_identifier: org } },
visible: isEnt && meta,
@@ -261,7 +261,7 @@ export default defineComponent({
{
key: "alert_destinations",
label: t("alert_destinations.header"),
description: "Where triggered alerts are delivered",
description: t("settings.alertDestinationsDesc"),
icon: "location-on",
to: { name: "alertDestinations", query: { org_identifier: org } },
dataTest: "alert-destinations-tab",
@@ -270,7 +270,7 @@ export default defineComponent({
{
key: "pipeline_destinations",
label: t("pipeline_destinations.header"),
description: "External targets for pipeline output",
description: t("settings.pipelineDestinationsDesc"),
icon: "person-pin-circle",
to: { name: "pipelineDestinations", query: { org_identifier: org } },
visible: isEnt,
@@ -280,7 +280,7 @@ export default defineComponent({
{
key: "templates",
label: t("alert_templates.header"),
description: "Reusable alert message templates",
description: t("settings.templatesDesc"),
icon: "description",
to: { name: "alertTemplates", query: { org_identifier: org } },
dataTest: "alert-templates-tab",
@@ -289,7 +289,7 @@ export default defineComponent({
{
key: "storageSettings",
label: t("storage_settings.tabLabel"),
description: "Per-organization storage configuration",
description: t("settings.storageSettingsDesc"),
icon: "cloud",
to: { name: "storageSettings", query: { org_identifier: org } },
visible:
@@ -303,7 +303,7 @@ export default defineComponent({
{
key: "model_pricing",
label: t("settings.llmModelPricing"),
description: "LLM model cost configuration",
description: t("settings.modelPricingDesc"),
icon: "paid",
to: { name: "modelPricing", query: { org_identifier: org } },
visible: !!z.model_pricing_enabled,
@@ -313,7 +313,7 @@ export default defineComponent({
{
key: "correlation_settings",
label: t("settings.correlationSettings"),
description: "Telemetry correlation configuration",
description: t("settings.correlationSettingsDesc"),
icon: "group-work",
to: { name: "correlationSettings", query: { org_identifier: org } },
visible: isEnt && z.service_streams_enabled !== false,
@@ -323,7 +323,7 @@ export default defineComponent({
{
key: "llm_providers",
label: t("llmProviders.tabLabel"),
description: "LLM providers for online evaluations",
description: t("settings.llmProvidersDesc"),
icon: "smart-toy",
to: { name: "llmProviders", query: { org_identifier: org } },
visible: (isEnt || isCloud) && !!z.online_evals_enabled,
@@ -333,7 +333,7 @@ export default defineComponent({
{
key: "queryManagement",
label: t("settings.queryManagement"),
description: "Inspect and cancel running queries",
description: t("settings.queryManagementDesc"),
icon: "query-stats",
to: `/settings/query_management?org_identifier=${org}`,
visible: isEnt && meta,
@@ -343,7 +343,7 @@ export default defineComponent({
{
key: "nodes",
label: t("settings.nodes"),
description: "Cluster nodes and their health",
description: t("settings.nodesDesc"),
icon: "hub",
to: { name: "nodes", query: { org_identifier: org } },
visible: isEnt && meta,
@@ -353,7 +353,7 @@ export default defineComponent({
{
key: "license",
label: t("settings.license"),
description: "Enterprise license details",
description: t("settings.licenseDesc"),
icon: "card-membership",
to: { name: "license", query: { org_identifier: org } },
visible: isEnt && meta,
@@ -363,7 +363,7 @@ export default defineComponent({
{
key: "organization_management",
label: t("settings.organizationManagement"),
description: "Create and manage organizations",
description: t("settings.organizationManagementDesc"),
icon: "lan",
to: { name: "orgnizationManagement", query: { org_identifier: org } },
visible: isCloud && meta,
@@ -392,9 +392,19 @@ export default defineComponent({
const i = settingsGroupOrder.indexOf(label);
return i === -1 ? Number.MAX_SAFE_INTEGER : i;
};
// Internal group keys stay English (used for bucketing + rank); only the
// displayed label is translated so sorting/ranking is unaffected.
const groupLabels: Record<string, string> = {
"General": t("settings.groupGeneral"),
"Access & Security": t("settings.groupAccessSecurity"),
"Destinations & Templates": t("settings.groupDestinationsTemplates"),
"Data & AI": t("settings.groupDataAI"),
"Operations": t("settings.groupOperations"),
"Account": t("settings.groupAccount"),
};
return [...buckets.keys()]
.sort((a, b) => rank(a) - rank(b))
.map((label) => ({ label, items: buckets.get(label)! }));
.map((label) => ({ label: groupLabels[label] ?? label, items: buckets.get(label)! }));
});
return {
@@ -4,6 +4,7 @@ import { mount, flushPromises } from "@vue/test-utils";
import { nextTick } from "vue";
import { createStore } from "vuex";
import { createI18n } from "vue-i18n";
import enLocale from "@/locales/languages/en.json";
import SettingsIndex from "./index.vue";
// Mock composables and config with factory functions
@@ -83,47 +84,13 @@ const mockStore = createStore({
actions: {},
});
// Use the real en.json so section/group labels resolve exactly as in the app.
// Group labels (e.g. "General", "Destinations & Templates") are now translated via
// t("settings.groupGeneral") etc.; sourcing the real dictionary keeps this spec in sync.
const mockI18n = createI18n({
locale: "en",
messages: {
en: {
settings: {
header: "Settings",
generalLabel: "General",
orgLabel: "Organization",
queryManagement: "Query Management",
cipherKeys: "Cipher Keys",
nodes: "Nodes",
ssoDomainRestrictions: "SSO Management",
organizationManagement: "Organization Management",
correlationSettings: "Correlation Settings",
llmModelPricing: "LLM Model Pricing",
license: "License",
},
alert_destinations: {
header: "Alert Destinations",
},
pipeline_destinations: {
header: "Pipeline Destinations",
},
alert_templates: {
header: "Alert Templates",
},
regex_patterns: {
header: "Regex Patterns",
title: "Regex Patterns",
},
storage_settings: {
tabLabel: "Storage Settings",
},
llmProviders: {
tabLabel: "LLM Providers",
},
cipherKey: {
add: "Add Cipher Key",
update: "Update Cipher Key",
},
},
en: enLocale,
},
});
@@ -151,7 +151,7 @@ the Free Software Foundation, either version 3 of the License, or
data-test="quality-refresh-btn"
@click="onQualityRefresh"
>
Refresh
{{ t('common.refresh') }}
</OButton>
</template>
</AppPageHeader>
@@ -137,6 +137,11 @@ const activeSectionItem = computed(() =>
// Group order: Monitor before Evaluate.
const sectionGroupOrder = ["Monitor", "Evaluate"];
const groupLabels = computed<Record<string, string>>(() => ({
Monitor: t("aiObservability.sections.monitor"),
Evaluate: t("aiObservability.sections.evaluate"),
}));
const sectionGroups = computed<SectionHubGroup[]>(() => {
const buckets = new Map<string, SectionHubItem[]>();
for (const item of sectionItems.value) {
@@ -150,7 +155,7 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
};
return [...buckets.keys()]
.sort((a, b) => rank(a) - rank(b))
.map((label) => ({ label, items: buckets.get(label)! }));
.map((key) => ({ label: groupLabels.value[key] ?? key, items: buckets.get(key)! }));
});
// Reserved for future per-section header chrome wiring (mirrors Settings'
@@ -1,12 +1,25 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mount } from "@vue/test-utils";
import { createI18n } from "vue-i18n";
import enLocale from "@/locales/languages/en.json";
import ORefreshButton from "./ORefreshButton.vue";
// The component calls useI18n() in setup, so an i18n instance must be installed
// or vue-i18n throws "Need to install with app.use function". Source the real
// en.json so refreshButton.* labels resolve as they do in the app.
const i18n = createI18n({
locale: "en",
legacy: false,
messages: { en: enLocale },
});
// Stub OButton to avoid rendering its internals
const stubs = {
OButton: { template: '<button v-bind="$attrs"><slot /></button>' },
};
const globalConfig = { stubs, plugins: [i18n] };
describe("ORefreshButton", () => {
beforeEach(() => {
vi.useFakeTimers();
@@ -18,12 +31,12 @@ describe("ORefreshButton", () => {
// --- Default rendering ---
it("renders the refresh button", () => {
const wrapper = mount(ORefreshButton, { global: { stubs } });
const wrapper = mount(ORefreshButton, { global: globalConfig });
expect(wrapper.find('[data-test="refresh-button"]').exists()).toBe(true);
});
it("does not render a timestamp span when lastRunAt is not provided", () => {
const wrapper = mount(ORefreshButton, { global: { stubs } });
const wrapper = mount(ORefreshButton, { global: globalConfig });
expect(wrapper.find("span.tw\:tabular-nums").exists()).toBe(false);
});
@@ -31,7 +44,7 @@ describe("ORefreshButton", () => {
const ts = Date.now() - 10_000;
const wrapper = mount(ORefreshButton, {
props: { lastRunAt: ts },
global: { stubs },
global: globalConfig,
});
expect(wrapper.find("span.tw\\:tabular-nums").exists()).toBe(true);
});
@@ -39,7 +52,7 @@ describe("ORefreshButton", () => {
// --- dot color classes ---
it("uses idle dot color when no lastRunAt", () => {
const wrapper = mount(ORefreshButton, { global: { stubs } });
const wrapper = mount(ORefreshButton, { global: globalConfig });
const dot = wrapper.find(".tw\\:rounded-full");
expect(dot.classes()).toContain("tw:bg-refresh-dot-idle");
});
@@ -47,7 +60,7 @@ describe("ORefreshButton", () => {
it("uses fresh dot color when lastRunAt < 30s ago", () => {
const wrapper = mount(ORefreshButton, {
props: { lastRunAt: Date.now() - 5_000 },
global: { stubs },
global: globalConfig,
});
const dot = wrapper.find(".tw\\:rounded-full");
expect(dot.classes()).toContain("tw:bg-refresh-dot-fresh");
@@ -56,7 +69,7 @@ describe("ORefreshButton", () => {
it("uses stale dot color when lastRunAt is 30s–5min ago", () => {
const wrapper = mount(ORefreshButton, {
props: { lastRunAt: Date.now() - 60_000 },
global: { stubs },
global: globalConfig,
});
const dot = wrapper.find(".tw\\:rounded-full");
expect(dot.classes()).toContain("tw:bg-refresh-dot-stale");
@@ -65,7 +78,7 @@ describe("ORefreshButton", () => {
it("uses critical dot color when lastRunAt > 5min ago", () => {
const wrapper = mount(ORefreshButton, {
props: { lastRunAt: Date.now() - 400_000 },
global: { stubs },
global: globalConfig,
});
const dot = wrapper.find(".tw\\:rounded-full");
expect(dot.classes()).toContain("tw:bg-refresh-dot-critical");
@@ -74,7 +87,7 @@ describe("ORefreshButton", () => {
it("uses idle dot color when loading=true regardless of lastRunAt", () => {
const wrapper = mount(ORefreshButton, {
props: { lastRunAt: Date.now() - 5_000, loading: true },
global: { stubs },
global: globalConfig,
});
const dot = wrapper.find(".tw\\:rounded-full");
expect(dot.classes()).toContain("tw:bg-refresh-dot-idle");
@@ -83,7 +96,7 @@ describe("ORefreshButton", () => {
// --- emits ---
it("emits click when button is clicked", async () => {
const wrapper = mount(ORefreshButton, { global: { stubs } });
const wrapper = mount(ORefreshButton, { global: globalConfig });
await wrapper.find('[data-test="refresh-button"]').trigger("click");
expect(wrapper.emitted("click")).toHaveLength(1);
});
@@ -91,7 +104,7 @@ describe("ORefreshButton", () => {
it("does not emit click when loading=true", async () => {
const wrapper = mount(ORefreshButton, {
props: { loading: true },
global: { stubs },
global: globalConfig,
});
await wrapper.find('[data-test="refresh-button"]').trigger("click");
expect(wrapper.emitted("click")).toBeUndefined();
@@ -100,7 +113,7 @@ describe("ORefreshButton", () => {
it("does not emit click when disabled=true", async () => {
const wrapper = mount(ORefreshButton, {
props: { disabled: true },
global: { stubs },
global: globalConfig,
});
await wrapper.find('[data-test="refresh-button"]').trigger("click");
expect(wrapper.emitted("click")).toBeUndefined();
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { useI18n } from "vue-i18n";
import OButton from "../Button/OButton.vue";
import type {
RefreshButtonProps,
RefreshButtonEmits,
} from "./ORefreshButton.types";
const { t } = useI18n();
const props = withDefaults(defineProps<RefreshButtonProps>(), {
lastRunAt: null,
loading: false,
@@ -24,11 +27,11 @@ const diffSeconds = (): number => {
const getRelativeTime = (ts: number): string => {
const sec = Math.floor((Date.now() - ts) / 1000);
if (sec < 5) return "just now";
if (sec < 60) return `${sec}s ago`;
if (sec < 5) return t("refreshButton.justNow");
if (sec < 60) return t("refreshButton.secondsAgo", { sec });
const min = Math.floor(sec / 60);
if (min < 60) return `${min}m ago`;
return `${Math.floor(min / 60)}h ago`;
if (min < 60) return t("refreshButton.minutesAgo", { min });
return t("refreshButton.hoursAgo", { h: Math.floor(min / 60) });
};
const updateRelativeTime = () => {
@@ -51,15 +54,17 @@ const dotColor = computed(() => {
const dotTitle = computed(() => {
const s = diffSeconds();
if (s === Infinity) return "Not yet refreshed";
if (s < 30) return "Data is fresh";
if (s < 300) return "Data is getting stale";
return "Data is stale";
if (s === Infinity) return t("refreshButton.notYetRefreshed");
if (s < 30) return t("refreshButton.dataFresh");
if (s < 300) return t("refreshButton.dataGettingStale");
return t("refreshButton.dataStale");
});
const exactTime = computed(() => {
if (!props.lastRunAt) return "Not yet refreshed";
return `Last refreshed: ${new Date(props.lastRunAt).toLocaleTimeString()}`;
if (!props.lastRunAt) return t("refreshButton.notYetRefreshed");
return t("refreshButton.lastRefreshed", {
time: new Date(props.lastRunAt).toLocaleTimeString(),
});
});
onMounted(() => {
@@ -95,7 +100,7 @@ function handleClick(e: MouseEvent) {
class="tw:text-xs tw:text-text-secondary tw:tabular-nums tw:whitespace-nowrap tw:select-none"
:title="exactTime"
>
{{ relativeTime || "just now" }}
{{ relativeTime || t("refreshButton.justNow") }}
</span>
<!-- refresh icon button -->
<OButton
File diff suppressed because it is too large Load Diff
+217 -6
View File
@@ -388,6 +388,7 @@
"nameRequired": "Name is required",
"edit": "Edit",
"delete": "Delete",
"export": "Export",
"actions": "Actions",
"version": "Version",
"updated_at": "Updated At",
@@ -522,6 +523,7 @@
"nlpModeLabel": "Natural Language Mode",
"quickModeLabel": "Quick Mode",
"disableQuickMode": "Disable Quick Mode",
"menuMore": "More",
"menuGroupSetOnce": "Set Once",
"menuGroupSavedViews": "Saved Views",
"menuGroupTools": "Tools",
@@ -741,7 +743,17 @@
"selectStream": "Select Stream",
"pattern_summary": "Showing 1 to 50 out of {totalEvents} events & {patternsFound} patterns found in {logsAnalyzed} events in {totalTime} ms.",
"selectLogStream": "Select logs stream",
"loadingStream": "Loading Stream..."
"loadingStream": "Loading Stream...",
"searchInspect": "Search Inspect",
"vrlOnlyForTableWarning": "VRL function is only supported for table chart.",
"customDownloadTitle": "Custom Download",
"selectFunctionNamePlaceholder": "Select Function Name",
"confirmFunctionUpdateTitle": "Confirm Update",
"confirmFunctionUpdateMsg": "Are you sure you want to update the function {name}?",
"traceIdLabel": "Trace ID:",
"enterTraceIdPlaceholder": "Enter trace ID",
"changeQueryModeTitle": "Change Query Mode",
"changeQueryModeConfirm": "Are you sure you want to change the query mode? The data saved for X-Axis, Y-Axis and Filters will be wiped off."
},
"menu": {
"home": "Home",
@@ -783,6 +795,13 @@
"performance": "Performance",
"sessions": "Sessions",
"errorTracking": "Error Tracking",
"sourceMaps": "Source Maps",
"service": "Service",
"environment": "Environment",
"applyFilters": "Apply Filters",
"uploadSourceMaps": "Upload Source Maps",
"files": "Files",
"uploadedAt": "Uploaded At",
"overview": "Overview",
"webVitals": "Web Vitals",
"errors": "Errors",
@@ -1183,6 +1202,7 @@
},
"user": {
"header": "Organization Member",
"subtitle": "People with access to this organization",
"id": "User ID",
"name": "User name",
"role": "Role",
@@ -1239,7 +1259,11 @@
"noCustomRolesHint": "No custom roles defined — create one in IAM → Roles",
"externalUserCustomRoleHint": "External users can only be modified via the source system",
"description": "Description",
"revoke_invite": "Revoke Invite"
"revoke_invite": "Revoke Invite",
"revokeInvitationTitle": "Revoke Invitation",
"revokeInvitationMsg": "Are you sure you want to revoke the invitation for {email}?",
"deleteUsersTitle": "Delete Users",
"deleteUsersMsg": "Are you sure you want to delete {count} user(s)?"
},
"invitation": {
"pendingInvitations": "Pending Invitations",
@@ -1279,6 +1303,18 @@
},
"iam": {
"users": "Users",
"sectionAccess": "Access",
"sectionPermissions": "Permissions",
"sectionOrganization": "Organization",
"usersDesc": "People with access to this organization",
"serviceAccountsDesc": "Programmatic access tokens for APIs",
"ingestionTokensDesc": "Tokens for ingesting data into this organization",
"invitationsDesc": "Pending and sent member invitations",
"groupsDesc": "Group users together to assign roles",
"rolesDesc": "Define permissions and access policies",
"quotaDesc": "Usage limits applied per role",
"organizationsDesc": "Organizations you can access",
"nameHelpText": "Use alphanumeric and '_' characters only, without spaces.",
"groups": "User Groups",
"roles": "Roles",
"organizations": "Organizations",
@@ -1367,6 +1403,7 @@
"getapikey": "Get API Key",
"role": "Role",
"nameRequired": "Name is required",
"nameHelpText": "Use alphanumeric characters, space and underscore only.",
"subscription_type": "Plan Type",
"proceed_subscription": "Proceed to Subscription",
"subscription_plan": "Subscription Plan",
@@ -1379,6 +1416,8 @@
},
"logStream": {
"header": "Streams",
"subtitle": "Index management and stream configuration",
"streamNameHelpText": "Use alphanumeric characters, underscore and colon only.",
"search": "Search Stream",
"name": "Name",
"docNum": "Events",
@@ -1398,6 +1437,7 @@
"confirmBatchDeleteHead": "Delete Streams",
"confirmDeleteMsg": "Are you sure you want to delete this stream?",
"confirmBatchDeleteMsg": "Are you sure you want to delete the selected streams?",
"deleteAssociatedAlertsPipelines": "Delete all Pipelines and Alerts associated with the stream",
"cancel": "Cancel",
"ok": "OK",
"docsCount": "Docs Count",
@@ -1611,6 +1651,9 @@
},
"function": {
"header": "Functions",
"subtitle": "Reusable VRL functions applied in pipelines",
"clickTestFunctionHint": "Please click Test Function to see the events",
"pleaseEnterQuery": "Please enter a query",
"add": "New function",
"search": "Search Function",
"name": "Enter Function Name",
@@ -1645,6 +1688,7 @@
"paramsHint": "param1, param2, etc.",
"associatedFunctionHeader": "Associated Functions",
"enrichmentTables": "Enrichment Tables",
"enrichmentTablesSubtitle": "Lookup tables that enrich ingested data",
"addEnrichmentTable": "New enrichment table",
"updateEnrichmentTable": "Update enrichment table",
"searchEnrichmentTable": "Search enrichment table",
@@ -1847,10 +1891,15 @@
"errorFetchingFunctions": "Error while fetching functions",
"functionAppliedSuccess": "{name} function applied successfully.",
"header": "Dashboards",
"subtitle": "Create, organize, and share dashboards across folders",
"add": "New dashboard",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete Dashboard",
"deleteDashboardConfirmTitle": "Delete dashboard",
"deleteDashboardConfirmMsg": "Are you sure you want to delete the dashboard?",
"deleteDashboardsConfirmTitle": "Delete Dashboards",
"deleteDashboardsConfirmMsg": "Are you sure you want to delete {count} dashboard(s)?",
"edit": "Edit Dashboard",
"duplicate": "Duplicate Dashboard",
"move_to_another_folder": "Move to another Folder",
@@ -1872,9 +1921,18 @@
"id": "Id",
"goBackToDashboard": "Go back to dashboard",
"import": "Import",
"importCustom": "Custom",
"importCustomDesc": "Import from JSON file or URL",
"importTemplates": "Templates",
"importTemplatesDesc": "Browse and import from gallery",
"searchThisFolder": "This folder",
"searchThisFolderTitle": "Search only this folder",
"searchAllFolders": "All folders",
"searchAllFoldersTitle": "Search across all folders",
"setting": "Dashboard Settings",
"print": "Print Dashboard",
"refresh": "Refresh",
"reloadDashboards": "Reload dashboards",
"export": "Export Dashboard",
"share": "Share",
"fullscreen": "Go Fullscreen",
@@ -1883,6 +1941,26 @@
"variableSettings": "Variables",
"tabSettings": "Tabs",
"newVariable": "Add Variable",
"editVariable": "Edit Variable",
"addOption": "Add Option",
"addFilter": "Add Filter",
"noDataFound": "No Data Found",
"streamSelectTooltip": "Select a stream or use a variable like $streamVariable to dynamically choose the stream based on another value.",
"fieldSelectTooltip": "Select a field or use a variable like $fieldVariable. If stream uses a variable, field list will be empty - type field name manually.",
"filterInfoTooltip": "In filters, you can use the value of another variable to filter the current variable's value. This can be done by using the other variable's name. For example:",
"defaultSelectAll": "Default - Select All",
"escapeSingleQuotesTooltip": "If enabled, single quotes will be escaped in the query. For example, a value like `O'Reilly` will be replaced as `O''Reilly`.",
"byDefaultSelect": "By Default Select:",
"firstValue": "First value",
"allValues": "All values",
"customValue": "Custom",
"showDependencies": "Show Dependencies",
"deleteVariable": "Delete Variable",
"deleteVariableMsg": "Are you sure you want to delete the variable?",
"appliedToTabs": "Applied to tabs:",
"appliedToPanels": "Applied to panels:",
"selectVariableScope": "Select variable scope",
"variableScopeHelp": "Variables will be applied to all tabs and panels if global is selected.",
"newTab": "Add Tab",
"generalSettingsTitle": "General Settings",
"tabSettingsTitle": "Tabs Settings",
@@ -2159,6 +2237,7 @@
"panelTimeEnabled": "Panel-Specific Time Range",
"useDefaultTimeTooltip": "When enabled, this panel will use the configured default duration as its time range on dashboard load, instead of the global dashboard time.",
"defaultDuration": "Default Duration",
"showDynamicFilters": "Show Dynamic Filters",
"addButton": "+ Add",
"fixed": "Fixed",
"byValue": "By Value",
@@ -2506,7 +2585,13 @@
"one": "Show me errors from the last hour…",
"two": "Write VRL to parse JSON from nginx logs…",
"three": "Build a dashboard for K8s node health…"
}
},
"autoNavigation": {
"label": "Auto Navigation",
"enabledTooltip": "Auto navigation enabled - O2 Assistant will auto navigate without confirmation",
"disabledTooltip": "Auto navigation disabled - O2 Assistant will ask before navigating"
},
"attachImageTooltip": "Attach images (PNG, JPEG, max 2MB)"
},
"billing": {
"header": "Usage & Billing",
@@ -2670,6 +2755,15 @@
},
"alerts": {
"header": "Alerts",
"subtitle": "Alert rules and notification channels",
"searchThisFolder": "This folder",
"searchAllFolders": "All folders",
"streamConfig": "Stream Config",
"alertRules": "Alert Rules",
"noTemplatesMsg": "It looks like you haven't created any Templates yet. To create an Alert, you'll need to have at least one Destination and one Template in place",
"createTemplateBtn": "Create Template",
"noDestinationsMsg": "It looks like you haven't created any Destinations yet. To create an Alert, you'll need to have at least one Destination and one Template in place",
"createDestinationBtn": "Create Destination",
"add": "New alert",
"edit": "Edit",
"clone": "Clone alert",
@@ -2677,8 +2771,19 @@
"move": "Move alert",
"name": "Name",
"addConditions": "Add Conditions",
"addCondition": "Add Condition",
"sqlEditor": "SQL Editor",
"promqlEditor": "PromQL Editor",
"conditionOf": "of",
"conditionIs": "is",
"matchingTypeFound": "matching {type} found",
"matchingMetricsFound": "matching metrics found",
"metricsValueFieldTooltip": "Metrics streams store their measurement in the \"value\" field by default",
"howOftenCheckTooltip": "How often to check this alert condition",
"alertIfNoOfEvents": "Alert if No. of events",
"alertIfValueIs": "Alert if the value is",
"havingSeries": "Having series",
"clearMultiWindowsTitle": "Clear Multi-Windows?",
"vrlEditor": "VRL Editor",
"generateVRL": "Generate VRL",
"applyVRL": "Apply VRL",
@@ -2844,7 +2949,8 @@
"createdBy": "Created By",
"createdAt": "Created At",
"lastRunAt": "Last Run At",
"lastSuccessfulAt": "Last Successful At",
"lastSuccessfulAt": "Last Successfull At",
"lastTrainedAt": "Last Trained At",
"status": "Status",
"numOfEvents": "Number of Events",
"view": "View",
@@ -3013,6 +3119,7 @@
},
"incidents": {
"title": "Incidents",
"subtitle": "Incident management and tracking",
"header": "Alert Incidents",
"search": "Search incidents...",
"noIncidents": "No incidents found",
@@ -3377,6 +3484,28 @@
"id": "Id",
"update": "Update",
"generalLabel": "General Settings",
"groupGeneral": "General",
"groupAccessSecurity": "Access & Security",
"groupDestinationsTemplates": "Destinations & Templates",
"groupDataAI": "Data & AI",
"groupOperations": "Operations",
"groupAccount": "Account",
"generalDesc": "Theme, query defaults, and core preferences",
"orgDesc": "Organization parameters and identifiers",
"cipherKeysDesc": "Encryption keys for sensitive fields",
"regexPatternsDesc": "Reusable regex patterns for redaction",
"domainManagementDesc": "Restrict sign-in to allowed SSO domains",
"alertDestinationsDesc": "Where triggered alerts are delivered",
"pipelineDestinationsDesc": "External targets for pipeline output",
"templatesDesc": "Reusable alert message templates",
"storageSettingsDesc": "Per-organization storage configuration",
"modelPricingDesc": "LLM model cost configuration",
"correlationSettingsDesc": "Telemetry correlation configuration",
"llmProvidersDesc": "LLM providers for online evaluations",
"queryManagementDesc": "Inspect and cancel running queries",
"nodesDesc": "Cluster nodes and their health",
"licenseDesc": "Enterprise license details",
"organizationManagementDesc": "Create and manage organizations",
"ingestionTokens": "Ingestion Tokens",
"queryManagement": "Query Management",
"apikeyLabel": "API Keys",
@@ -3682,6 +3811,13 @@
},
"reports": {
"header": "Reports",
"subtitle": "Scheduled reports and exports",
"searchThisFolder": "This folder",
"searchAllFolders": "All folders",
"reloadReports": "Reload reports",
"moveToFolder": "Move to Folder",
"deleteReportsTitle": "Delete Reports",
"deleteReportsMsg": "Are you sure you want to delete {count} report(s)?",
"add": "New report",
"search": "Search report",
"update": "Update Report",
@@ -3987,7 +4123,21 @@
"legend": {
"title": "Error Rate"
}
}
},
"spansTab": "Spans",
"tracesTab": "Traces",
"serviceGraphTab": "Service Graph",
"treeView": "Tree View",
"graphView": "Graph View",
"layoutHorizontal": "Horizontal",
"layoutVertical": "Vertical",
"layoutForce": "Force Layout",
"exportTraces": "Export Traces",
"waterfall": "Waterfall",
"flameGraph": "Flame Graph",
"traceGraph": "Trace Graph",
"dag": "DAG",
"thread": "Thread"
},
"actions": {
"testAction": "Test action",
@@ -4173,7 +4323,38 @@
"failed_to_load": "Failed to load patterns",
"no_patterns_selected": "Please select at least one pattern",
"name_required": "* Name is required",
"pattern_required": "* Pattern is required"
"pattern_required": "* Pattern is required",
"associated_breadcrumb": "Stream Details",
"associated_search_placeholder": "Search",
"applied_patterns_count": "Applied Patterns ({count})",
"all_patterns_count": "All Patterns ({count})",
"no_data_available": "No data available",
"no_patterns_applied_title": "No Patterns Applied Yet",
"no_patterns_applied_subtitle": "Browse the pattern library to begin applying regular expressions to your fields.",
"pattern_name": "Pattern Name",
"when_value_matches": "When value matches",
"redact": "Redact",
"redact_hint": "Replace with [REDACTED]",
"drop": "Drop",
"drop_hint": "Drop the field completely",
"hash": "Hash",
"hash_hint": "Replace with searchable hash",
"detect_at": "Detect at",
"ingestion": "Ingestion",
"query": "Query",
"test_pattern": "Test Pattern",
"test_input": "Test Input",
"regex_pattern_label": "Regex Pattern",
"input_string": "Input string",
"input_string_placeholder": "Eg. 1234567890",
"output": "Output",
"output_string_placeholder": "Output String",
"click_test_input_hint": "Please click Test Input to see the results",
"remove_pattern": "Remove Pattern",
"add_pattern": "Add Pattern",
"remove_pattern_confirm_msg": "Are you sure you want to remove this pattern from the field?",
"failed_to_test_string": "Failed to test string",
"select_detect_at_option": "Please select detect at option"
},
"search_history": {
"title": "Search History",
@@ -4577,6 +4758,24 @@
"evalJobs": "Eval Jobs",
"scorers": "Scorers",
"scoreConfigs": "Score Configs"
},
"kpi": {
"totalCost": "Total Cost",
"totalTokens": "Total Tokens",
"totalTraces": "Total Traces",
"p95Latency": "P95 Latency",
"errorRate": "Error Rate"
},
"panels": {
"costTrend": { "title": "Cost trend", "subtitle": "USD by model" },
"tokenTrend": { "title": "Token trend", "subtitle": "tokens by model" },
"spanTrend": { "title": "Span trend", "subtitle": "span count by kind" },
"latencyPercentiles": { "title": "Latency p50 / p95 / p99", "subtitle": "all LLM calls" },
"tracesOverTime": { "title": "Traces over time", "subtitle": "trace count by service" },
"errorsOverTime": { "title": "Errors over time", "subtitle": "error count" },
"spansByModel": { "title": "Spans by model", "subtitle": "call count per model" },
"tokensByModel": { "title": "Tokens by model", "subtitle": "total tokens per model" },
"recentErrors": { "title": "Recent errors", "subtitle": "last 10 failed spans" }
}
},
"onlineEvals": {
@@ -5393,6 +5592,7 @@
"descNoFilters": "No events were found in {range}. Try widening the time range to load results.",
"descWithFilters": "Nothing matched <strong>this query</strong> in the selected window. The stream has data here — try relaxing a filter in the query bar.",
"descOutOfRange": "Your time window doesn't contain any data from this stream. Jump to when data was last seen.",
"descDataAtBoundary": "The stream has data in this window but no events matched. Try checking your search history or asking AI for help.",
"jumpToData": "Jump to latest data",
"expandRange": "Expand the time range",
"expandRangeDescAbsolute": "Switch to a wider relative time range",
@@ -5467,5 +5667,16 @@
"askAi": "Ask AI to fix my query",
"showDetail": "Show details",
"hideDetail": "Hide details"
},
"refreshButton": {
"justNow": "just now",
"secondsAgo": "{sec}s ago",
"minutesAgo": "{min}m ago",
"hoursAgo": "{h}h ago",
"notYetRefreshed": "Not yet refreshed",
"dataFresh": "Data is fresh",
"dataGettingStale": "Data is getting stale",
"dataStale": "Data is stale",
"lastRefreshed": "Last refreshed: {time}"
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -147,7 +147,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="outline"
size="xs"
>
More
{{ t('search.menuMore') }}
</OButton>
</template>
@@ -463,7 +463,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OIcon name="troubleshoot" size="sm" />
</span>
</template>
<span data-test="search-inspect-label">Search Inspect</span>
<span data-test="search-inspect-label">{{ t('search.searchInspect') }}</span>
</ODropdownItem>
</ODropdownGroup>
@@ -1135,7 +1135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OIcon name="warning" size="md" class="tw:mx-2" />
<span
class="tw:text-red-500 tw:p-2 tw:font-semibold"
>VRL function is only supported for table chart.</span
>{{ t('search.vrlOnlyForTableWarning') }}</span
>
</div>
</div>
@@ -1164,7 +1164,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
ref="confirmDialog"
v-model:open="confirmDialogVisible"
size="xs"
title="Confirm"
:title="t('common.confirm')"
:secondary-button-label="t('confirmDialog.cancel')"
:primary-button-label="t('confirmDialog.ok')"
@click:secondary="cancelConfirmDialog"
@@ -1189,7 +1189,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="search-bar-custom-download-dialog"
v-model:open="customDownloadDialog"
size="md"
title="Custom Download"
:title="t('search.customDownloadTitle')"
:secondary-button-label="t('confirmDialog.cancel')"
:primary-button-label="t('search.btnDownload')"
@click:secondary="customDownloadDialog = false"
@@ -1287,8 +1287,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="tw:mb-3"
@update:model-value="isSavedFunctionAction = $event; savedFunctionName = ''"
>
<OToggleGroupItem value="update" size="sm">Update</OToggleGroupItem>
<OToggleGroupItem value="create" size="sm">Create</OToggleGroupItem>
<OToggleGroupItem value="update" size="sm">{{ t('common.update') }}</OToggleGroupItem>
<OToggleGroupItem value="create" size="sm">{{ t('common.create') }}</OToggleGroupItem>
</OToggleGroup>
<div v-if="isSavedFunctionAction == 'create'">
<OInput
@@ -1308,7 +1308,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
labelKey="name"
valueKey="name"
:label="t('search.saveFunctionName')"
:placeholder="'Select Function Name'"
:placeholder="t('search.selectFunctionNamePlaceholder')"
class="tw:py-2"
:error="!!savedFunctionSelectError"
:error-message="savedFunctionSelectError"
@@ -1320,8 +1320,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Function update confirmation dialog -->
<ConfirmDialog
data-test="search-bar-function-update-confirm-dialog"
title="Confirm Update"
:message="`Are you sure you want to update the function ${savedFunctionSelectedName}?`"
:title="t('search.confirmFunctionUpdateTitle')"
:message="t('search.confirmFunctionUpdateMsg', { name: savedFunctionSelectedName })"
v-model="functionUpdateConfirm"
@update:ok="executeFunctionUpdate"
@update:cancel="functionUpdateConfirm = false"
@@ -1370,39 +1370,39 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="search-bar-search-inspect-dialog"
v-model:open="searchInspectDialog"
size="sm"
title="Search Inspect"
:title="t('search.searchInspect')"
:secondary-button-label="t('confirmDialog.cancel')"
:primary-button-label="t('confirmDialog.ok')"
:primary-button-disabled="!searchInspectTraceId.trim()"
@click:secondary="searchInspectDialog = false"
@click:primary="navigateToSearchInspect"
>
<div class="tw:text-left tw:mb-1">Trace ID:</div>
<div class="tw:text-left tw:mb-1">{{ t('search.traceIdLabel') }}</div>
<OInput
v-model="searchInspectTraceId"
placeholder="Enter trace ID"
:placeholder="t('search.enterTraceIdPlaceholder')"
autofocus
data-test="search-inspect-trace-id-input"
/>
</ODialog>
<ConfirmDialog
title="Change Query Mode"
message="Are you sure you want to change the query mode? The data saved for X-Axis, Y-Axis and Filters will be wiped off."
:title="t('search.changeQueryModeTitle')"
:message="t('search.changeQueryModeConfirm')"
@update:ok="confirmBuildModeChangeOk"
@update:cancel="confirmBuildModeChange = false"
v-model="confirmBuildModeChange"
/>
<ConfirmDialog
title="Delete Saved View"
message="Are you sure you want to delete saved view?"
:title="t('search.deleteSavedView')"
:message="t('search.deleteSavedViewConfirm')"
@update:ok="confirmDeleteSavedViews"
@update:cancel="confirmDelete = false"
v-model="confirmDelete"
/>
<ConfirmDialog
title="Update Saved View"
message="Are you sure you want to update the saved view? This action will overwrite the existing one."
:title="t('search.updateSavedView')"
:message="t('search.updateSavedViewConfirm')"
@update:ok="confirmUpdateSavedViews"
@update:cancel="confirmUpdate = false"
v-model="confirmUpdate"
@@ -128,7 +128,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Trend panels (config-driven) -->
<div class="tw:grid tw:grid-cols-2 tw:gap-[0.625rem]">
<div
v-for="panel in LLM_INSIGHTS_PANELS"
v-for="panel in llmInsightsPanels"
:key="panel.id"
:class="panel.layout.colSpan === 2 ? 'tw:col-span-2' : ''"
>
@@ -168,7 +168,7 @@ import EvalEmptyState from "@/components/EvalEmptyState.vue";
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import { LLM_INSIGHTS_PANELS } from "./config/llmInsightsPanels";
import { getLLMInsightsPanels } from "./config/llmInsightsPanels";
import useStreams from "@/composables/useStreams";
const { getStreams } = useStreams();
@@ -176,6 +176,8 @@ const { t } = useI18n();
const router = useRouter();
const store = useStore();
const llmInsightsPanels = computed(() => getLLMInsightsPanels(t));
interface Props {
streamName: string;
startTime: number; // microseconds
@@ -321,7 +323,7 @@ const kpiCards = computed<KpiCard[]>(() => {
);
const costCard: KpiCard = {
label: "Total Cost",
label: t("aiObservability.kpi.totalCost"),
...splitCost(kpi.value.totalCost),
trend: costTrend,
sparkData: sparklines.value.cost,
@@ -331,7 +333,7 @@ const kpiCards = computed<KpiCard[]>(() => {
return [
costCard,
{
label: "Total Tokens",
label: t("aiObservability.kpi.totalTokens"),
value: tokens.value,
unit: tokens.unit,
trend: computeTrend(kpi.value.totalTokens, kpiPrev.value.totalTokens, true),
@@ -339,7 +341,7 @@ const kpiCards = computed<KpiCard[]>(() => {
sparkColor: "#a855f7",
},
{
label: "Total Traces",
label: t("aiObservability.kpi.totalTraces"),
value: traces.value,
unit: traces.unit,
trend: computeTrend(kpi.value.traceCount, kpiPrev.value.traceCount, true),
@@ -347,7 +349,7 @@ const kpiCards = computed<KpiCard[]>(() => {
sparkColor: "#3b82f6",
},
{
label: "P95 Latency",
label: t("aiObservability.kpi.p95Latency"),
value: p95.value,
unit: p95.unit,
trend: computeTrend(
@@ -359,7 +361,7 @@ const kpiCards = computed<KpiCard[]>(() => {
sparkColor: "#f97316",
},
{
label: "Error Rate",
label: t("aiObservability.kpi.errorRate"),
value: errorRate.toFixed(1),
unit: "%",
trend: computeTrend(errorRate, errorRatePrev, true),
+3 -3
View File
@@ -834,7 +834,7 @@ describe("SearchBar", () => {
await flushPromises();
// OButton: the download button uses title="Export Traces" (no .download-logs-btn class)
expect(wrapper.find('[title="Export Traces"]').exists()).toBe(true);
expect(wrapper.find('[title="traces.exportTraces"]').exists()).toBe(true);
});
it("should be disabled when queryResults.hits is empty", async () => {
@@ -843,7 +843,7 @@ describe("SearchBar", () => {
await flushPromises();
// OButton uses native HTML disabled attribute (not a CSS "disabled" class)
const btn = wrapper.find('[title="Export Traces"]');
const btn = wrapper.find('[title="traces.exportTraces"]');
expect(btn.exists()).toBe(true);
expect(btn.attributes("disabled")).toBeDefined();
});
@@ -861,7 +861,7 @@ describe("SearchBar", () => {
await flushPromises();
// OButton: when enabled, native disabled attribute should be absent
const btn = wrapper.find('[title="Export Traces"]');
const btn = wrapper.find('[title="traces.exportTraces"]');
expect(btn.exists()).toBe(true);
expect(btn.attributes("disabled")).toBeUndefined();
});
+13 -13
View File
@@ -27,35 +27,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="traces-search-mode-spans-btn"
value="spans"
size="sm"
:tooltip="shouldHideToggleText ? 'Spans' : undefined"
:tooltip="shouldHideToggleText ? t('traces.spansTab') : undefined"
>
<template #icon-left
><OIcon name="layers" size="sm" class="tw:shrink-0"
/></template>
<span v-if="!shouldHideToggleText">Spans</span>
<span v-if="!shouldHideToggleText">{{ t('traces.spansTab') }}</span>
</OToggleGroupItem>
<OToggleGroupItem
data-test="traces-search-mode-traces-btn"
value="traces"
size="sm"
:tooltip="shouldHideToggleText ? 'Traces' : undefined"
:tooltip="shouldHideToggleText ? t('traces.tracesTab') : undefined"
>
<template #icon-left
><OIcon name="account-tree" size="sm" class="tw:shrink-0"
/></template>
<span v-if="!shouldHideToggleText">Traces</span>
<span v-if="!shouldHideToggleText">{{ t('traces.tracesTab') }}</span>
</OToggleGroupItem>
<OToggleGroupItem
v-if="config.isEnterprise == 'true'"
data-test="traces-service-graph-toggle"
value="service-graph"
size="sm"
:tooltip="shouldHideToggleText ? 'Service Graph' : undefined"
:tooltip="shouldHideToggleText ? t('traces.serviceGraphTab') : undefined"
>
<template #icon-left
><OIcon name="share" size="sm" class="tw:shrink-0"
/></template>
<span v-if="!shouldHideToggleText">Service Graph</span>
<span v-if="!shouldHideToggleText">{{ t('traces.serviceGraphTab') }}</span>
</OToggleGroupItem>
<OToggleGroupItem
data-test="traces-search-mode-services-catalog-btn"
@@ -117,7 +117,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="xs"
icon-left="more-horiz"
>
More
{{ t('search.menuMore') }}
</OButton>
</template>
@@ -257,7 +257,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="outline"
size="icon-toolbar"
:disabled="!searchObj.data.queryResults?.hits?.length"
title="Export Traces"
:title="t('traces.exportTraces')"
@click="downloadLogs"
>
<OIcon name="download" size="sm" />
@@ -311,7 +311,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #icon-left>
<OIcon name="git-branch" size="sm" />
</template>
Tree View
{{ t('traces.treeView') }}
</OToggleGroupItem>
<OToggleGroupItem
data-test="service-graph-graph-view-btn"
@@ -320,7 +320,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<template #icon-left
><OIcon name="share" size="sm" class="tw:shrink-0" /></template>
Graph View
{{ t('traces.graphView') }}
</OToggleGroupItem>
</OToggleGroup>
<OSelect
@@ -921,11 +921,11 @@ export default defineComponent({
const serviceGraphLayoutOptions = computed(() => {
if (searchObj.meta.serviceGraphVisualizationType === "graph") {
return [{ label: "Force Layout", value: "force" }];
return [{ label: t("traces.layoutForce"), value: "force" }];
}
return [
{ label: "Horizontal", value: "horizontal" },
{ label: "Vertical", value: "vertical" },
{ label: t("traces.layoutHorizontal"), value: "horizontal" },
{ label: t("traces.layoutVertical"), value: "vertical" },
];
});
+5 -5
View File
@@ -245,19 +245,19 @@ size="xs"
<template #icon-left
><OIcon name="align-left" size="sm" class="tw:shrink-0"
/></template>
Waterfall
{{ t('traces.waterfall') }}
</OToggleGroupItem>
<OToggleGroupItem value="flame-graph" size="sm">
<template #icon-left>
<OIcon name="flame" size="sm" />
</template>
Flame Graph
{{ t('traces.flameGraph') }}
</OToggleGroupItem>
<OToggleGroupItem value="map" size="sm">
<template #icon-left
><OIcon name="account-tree" size="sm" class="tw:shrink-0"
/></template>
Trace Graph
{{ t('traces.traceGraph') }}
</OToggleGroupItem>
<OToggleGroupItem v-if="hasLLMSpans"
value="dag"
@@ -265,7 +265,7 @@ size="sm">
<template #icon-left>
<OIcon name="git-branch" size="sm" />
</template>
DAG
{{ t('traces.dag') }}
</OToggleGroupItem>
<!--
Thread tab gated on:
@@ -284,7 +284,7 @@ size="sm">
<template #icon-left
><OIcon name="chat" size="xs" class="tw:shrink-0"
/></template>
Thread
{{ t('traces.thread') }}
</OToggleGroupItem>
</OToggleGroup>
</div>
@@ -179,7 +179,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click.stop="viewSpanLogs"
data-test="trace-details-sidebar-header-toolbar-view-logs-btn"
>
View Logs
{{ t('traces.viewLogs') }}
</OButton>
<OTooltip :content="viewLogsTooltipContent" />
</span>
@@ -3,18 +3,20 @@
// Tests for the LLM Insights panel registry + helpers.
//
// Coverage focuses on the two pure helpers (`renderPanelSql` and
// `pickInterval`) plus invariants on the static `LLM_INSIGHTS_PANELS`
// array that the renderer depends on (e.g. unique IDs, valid layout
// `pickInterval`) plus invariants on the `getLLMInsightsPanels`
// registry that the renderer depends on (e.g. unique IDs, valid layout
// spans, table panels declaring columns).
import { describe, it, expect } from "vitest";
import {
LLM_INSIGHTS_PANELS,
getLLMInsightsPanels,
renderPanelSql,
pickInterval,
type LLMPanelDef,
} from "./llmInsightsPanels";
const LLM_INSIGHTS_PANELS = getLLMInsightsPanels((key) => key);
describe("renderPanelSql", () => {
// The placeholders are global — every occurrence must be replaced, not
// just the first. SQL templates often reference {{stream}} multiple
@@ -122,11 +122,12 @@ const TOKENS_FIELD = `gen_ai_usage_total_tokens`;
const MODEL_FIELD = `gen_ai_response_model`;
const OBSERVATION_TYPE_FIELD = `gen_ai_operation_name`;
export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
export function getLLMInsightsPanels(t: (key: string) => string): LLMPanelDef[] {
return [
{
id: "cost-trend",
title: "Cost trend",
subtitle: "USD by model",
title: t("aiObservability.panels.costTrend.title"),
subtitle: t("aiObservability.panels.costTrend.subtitle"),
type: "stacked-area",
layout: { colSpan: 1 },
query: {
@@ -152,8 +153,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "token-trend",
title: "Token trend",
subtitle: "tokens by model",
title: t("aiObservability.panels.tokenTrend.title"),
subtitle: t("aiObservability.panels.tokenTrend.subtitle"),
type: "stacked-area",
layout: { colSpan: 1 },
query: {
@@ -175,8 +176,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "span-trend",
title: "Span trend",
subtitle: "span count by kind",
title: t("aiObservability.panels.spanTrend.title"),
subtitle: t("aiObservability.panels.spanTrend.subtitle"),
type: "stacked-area",
layout: { colSpan: 1 },
query: {
@@ -197,8 +198,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "latency-percentiles",
title: "Latency p50 / p95 / p99",
subtitle: "all LLM calls",
title: t("aiObservability.panels.latencyPercentiles.title"),
subtitle: t("aiObservability.panels.latencyPercentiles.subtitle"),
type: "histogram-with-thresholds",
layout: { colSpan: 1 },
query: {
@@ -231,8 +232,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "traces-over-time",
title: "Traces over time",
subtitle: "trace count by service",
title: t("aiObservability.panels.tracesOverTime.title"),
subtitle: t("aiObservability.panels.tracesOverTime.subtitle"),
type: "stacked-area",
layout: { colSpan: 1 },
query: {
@@ -253,8 +254,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "errors-over-time",
title: "Errors over time",
subtitle: "error count",
title: t("aiObservability.panels.errorsOverTime.title"),
subtitle: t("aiObservability.panels.errorsOverTime.subtitle"),
type: "stacked-area",
layout: { colSpan: 1 },
color: "#ef4444",
@@ -280,8 +281,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "spans-by-model",
title: "Spans by model",
subtitle: "call count per model",
title: t("aiObservability.panels.spansByModel.title"),
subtitle: t("aiObservability.panels.spansByModel.subtitle"),
type: "horizontal-bar",
layout: { colSpan: 1 },
color: "#3b82f6",
@@ -302,8 +303,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "tokens-by-model",
title: "Tokens by model",
subtitle: "total tokens per model",
title: t("aiObservability.panels.tokensByModel.title"),
subtitle: t("aiObservability.panels.tokensByModel.subtitle"),
type: "horizontal-bar",
layout: { colSpan: 1 },
color: "#a855f7",
@@ -324,8 +325,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
},
{
id: "recent-errors",
title: "Recent errors",
subtitle: "last 10 failed spans",
title: t("aiObservability.panels.recentErrors.title"),
subtitle: t("aiObservability.panels.recentErrors.subtitle"),
type: "table",
layout: { colSpan: 2 },
emptyStateText: "No errors in this time range",
@@ -360,7 +361,8 @@ export const LLM_INSIGHTS_PANELS: LLMPanelDef[] = [
{ field: "trace_id", label: "", format: "view-link", align: "right" },
],
},
];
];
}
/**
* Substitute the standard placeholders in a SQL template with concrete values.
@@ -193,7 +193,7 @@ describe("DashboardJsonEditor", () => {
expect(drawer.exists()).toBe(true);
expect(drawer.props("open")).toBe(true);
expect(drawer.props("width")).toBe(70);
expect(drawer.props("title")).toBe("Edit Dashboard JSON");
expect(drawer.props("title")).toBe("dashboard.editJson");
expect(drawer.props("primaryButtonLabel")).toBe("common.save");
expect(drawer.props("secondaryButtonLabel")).toBe("common.cancel");
expect(drawer.props("primaryButtonLoading")).toBe(false);
@@ -3,7 +3,7 @@
:open="open"
:width="70"
persistent
title="Edit Dashboard JSON"
:title="t('dashboard.editJson')"
:secondary-button-label="t('common.cancel')"
:primary-button-label="t('common.save')"
:primary-button-loading="saveJsonLoading"
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<ODrawer data-test="dashboard-settings-drawer"
:open="open"
:width="74"
title="Dashboard Settings"
:title="t('dashboard.setting')"
@update:open="$emit('update:open', $event)"
>
<div
+19 -19
View File
@@ -29,7 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<AppPageHeader
:title="t('dashboard.header')"
icon="dashboard"
subtitle="Create, organize, and share dashboards across folders"
:subtitle="t('dashboard.subtitle')"
>
<template #actions>
<!-- import dashboard button with dropdown -->
@@ -50,9 +50,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-import-custom"
>
<div class="tw:flex tw:flex-col">
<span>Custom</span>
<span>{{ t('dashboard.importCustom') }}</span>
<span class="tw:text-xs tw:text-dropdown-item-text tw:opacity-60"
>Import from JSON file or URL</span
>{{ t('dashboard.importCustomDesc') }}</span
>
</div>
</ODropdownItem>
@@ -61,9 +61,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-import-templates"
>
<div class="tw:flex tw:flex-col">
<span>Templates</span>
<span>{{ t('dashboard.importTemplates') }}</span>
<span class="tw:text-xs tw:text-dropdown-item-text tw:opacity-60"
>Browse and import from gallery</span
>{{ t('dashboard.importTemplatesDesc') }}</span
>
</div>
</ODropdownItem>
@@ -149,15 +149,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="xs"
icon-left="folder-outline"
data-test="dashboard-search-scope-current"
title="Search only this folder"
>This folder</OToggleGroupItem>
:title="t('dashboard.searchThisFolderTitle')"
>{{ t('dashboard.searchThisFolder') }}</OToggleGroupItem>
<OToggleGroupItem
value="all"
size="xs"
icon-left="search"
data-test="dashboard-search-across-folders-toggle"
title="Search across all folders"
>All folders</OToggleGroupItem>
:title="t('dashboard.searchAllFoldersTitle')"
>{{ t('dashboard.searchAllFolders') }}</OToggleGroupItem>
</OToggleGroup>
</template>
</OInput>
@@ -171,7 +171,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="icon-sm"
icon-left="refresh"
:loading="loading"
title="Reload dashboards"
:title="t('dashboard.reloadDashboards')"
data-test="dashboard-list-refresh"
@click="getDashboards"
/>
@@ -303,7 +303,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click="moveMultipleDashboards"
icon-left="drive-file-move"
>
Move
{{ t('common.move') }}
</OButton>
<OButton
variant="outline"
@@ -312,7 +312,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-list-export-dashboards-btn"
@click="multipleExportDashboard"
>
Export
{{ t('common.export') }}
</OButton>
<OButton
variant="outline-destructive"
@@ -321,7 +321,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-list-delete-dashboards-btn"
@click="openBulkDeleteDialog"
>
Delete
{{ t('common.delete') }}
</OButton>
</div>
</div>
@@ -390,9 +390,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- delete dashboard dialog -->
<ConfirmDialog
title="Delete dashboard"
:title="t('dashboard.deleteDashboardConfirmTitle')"
data-test="dashboard-confirm-dialog"
message="Are you sure you want to delete the dashboard?"
:message="t('dashboard.deleteDashboardConfirmMsg')"
@update:ok="deleteDashboard"
@update:cancel="confirmDeleteDialog = false"
v-model="confirmDeleteDialog"
@@ -400,9 +400,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- delete folder dialog -->
<ConfirmDialog
title="Delete Folder"
:title="t('dashboard.deleteFolder')"
data-test="dashboard-confirm-delete-folder-dialog"
message="Are you sure you want to delete this Folder?"
:message="t('dashboard.deleteFolderMessage')"
@update:ok="deleteFolder"
@update:cancel="confirmDeleteFolderDialog = false"
v-model="confirmDeleteFolderDialog"
@@ -410,9 +410,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- bulk delete dashboards dialog -->
<ConfirmDialog
title="Delete Dashboards"
:title="t('dashboard.deleteDashboardsConfirmTitle')"
data-test="dashboard-confirm-bulk-delete-dialog"
:message="`Are you sure you want to delete ${selectedIds.length} dashboard(s)?`"
:message="t('dashboard.deleteDashboardsConfirmMsg', { count: selectedIds.length })"
@update:ok="bulkDeleteDashboards"
@update:cancel="confirmBulkDelete = false"
v-model="confirmBulkDelete"
+11 -11
View File
@@ -83,12 +83,12 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
const groups: { label: string; items: SectionHubItem[] }[] = [
{
label: "Access",
label: t("iam.sectionAccess"),
items: [
{
key: "users",
label: t("iam.basicUsers"),
description: "People with access to this organization",
description: t("iam.usersDesc"),
icon: "person",
to: { name: "users", query: orgQuery.value },
dataTest: "iam-users-tab",
@@ -96,7 +96,7 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
{
key: "serviceAccounts",
label: t("iam.serviceAccounts"),
description: "Programmatic access tokens for APIs",
description: t("iam.serviceAccountsDesc"),
icon: "manage-accounts",
to: { name: "serviceAccounts", query: orgQuery.value },
visible: svc,
@@ -105,7 +105,7 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
{
key: "ingestionTokens",
label: t("iam.ingestionTokens"),
description: "Tokens for ingesting data into this organization",
description: t("iam.ingestionTokensDesc"),
icon: "key",
to: { name: "ingestionTokens", query: orgQuery.value },
dataTest: "iam-ingestion-tokens-tab",
@@ -113,7 +113,7 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
{
key: "invitations",
label: t("iam.invitations"),
description: "Pending and sent member invitations",
description: t("iam.invitationsDesc"),
icon: "mail",
to: { name: "invitations", query: orgQuery.value },
visible: isCloud,
@@ -122,12 +122,12 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
],
},
{
label: "Permissions",
label: t("iam.sectionPermissions"),
items: [
{
key: "groups",
label: t("iam.groups"),
description: "Group users together to assign roles",
description: t("iam.groupsDesc"),
icon: "group",
to: { name: "groups", query: orgQuery.value },
visible: isEnt && rbac,
@@ -136,7 +136,7 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
{
key: "roles",
label: t("iam.roles"),
description: "Define permissions and access policies",
description: t("iam.rolesDesc"),
icon: "shield",
to: { name: "roles", query: orgQuery.value },
visible: isEnt && rbac,
@@ -145,7 +145,7 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
{
key: "quota",
label: t("iam.quota"),
description: "Usage limits applied per role",
description: t("iam.quotaDesc"),
icon: "speed",
to: { name: "quota", query: orgQuery.value },
visible: isEnt && rbac && meta,
@@ -154,12 +154,12 @@ const sectionGroups = computed<SectionHubGroup[]>(() => {
],
},
{
label: "Organization",
label: t("iam.sectionOrganization"),
items: [
{
key: "organizations",
label: t("iam.organizations"),
description: "Organizations you can access",
description: t("iam.organizationsDesc"),
icon: "corporate-fare",
to: { name: "organizations", query: orgQuery.value },
dataTest: "iam-organizations-tab",
+2 -2
View File
@@ -24,7 +24,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- Row 1: standard header — title + actions only. The stream-type filter
and search moved into the table's own toolbar below. -->
<template #header>
<AppPageHeader :subtitle="'Index management and stream configuration'" icon="window">
<AppPageHeader :subtitle="t('logStream.subtitle')" icon="window">
<template #title><span data-test="log-stream-title-text">{{ t('logStream.header') }}</span></template>
<template #actions>
<OButton
@@ -222,7 +222,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-model="deleteAssociatedAlertsPipelines"
/>
<span class="delete-associated-alerts-pipelines-text">
Delete all Pipelines and Alerts associated with the stream
{{ t("logStream.deleteAssociatedAlertsPipelines") }}
</span>
</div>
</div>
+1 -1
View File
@@ -183,7 +183,7 @@ const tabs = [
value: "error_tracking",
},
{
label: "Source Maps",
label: t("rum.sourceMaps"),
value: "source_maps",
},
];
+2 -2
View File
@@ -539,8 +539,8 @@ describe("SourceMaps.vue", () => {
expect(dialog.exists()).toBe(true);
expect(dialog.props("open")).toBe(false);
expect(dialog.props("size")).toBe("xs");
expect(dialog.props("primaryButtonLabel")).toBe("OK");
expect(dialog.props("secondaryButtonLabel")).toBe("Cancel");
expect(dialog.props("primaryButtonLabel")).toBe("common.ok");
expect(dialog.props("secondaryButtonLabel")).toBe("common.cancel");
});
it("confirmDeleteSourceMap opens dialog and populates title/message/data", async () => {
+19 -17
View File
@@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSelect
v-model="filters.version"
:options="versionOptions"
label="Version"
:label="t('common.version')"
clearable
searchable
creatable
@@ -38,7 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSelect
v-model="filters.service"
:options="serviceOptions"
label="Service"
:label="t('rum.service')"
clearable
searchable
creatable
@@ -50,7 +50,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSelect
v-model="filters.environment"
:options="environmentOptions"
label="Environment"
:label="t('rum.environment')"
clearable
searchable
creatable
@@ -64,7 +64,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="sm-action"
@click="applyFilters"
:loading="isLoading"
>Apply Filters</OButton>
>{{ t('rum.applyFilters') }}</OButton>
</div>
@@ -74,7 +74,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
variant="outline"
size="sm-action"
@click="navigateToUpload"
>Upload Source Maps</OButton>
>{{ t('rum.uploadSourceMaps') }}</OButton>
</div>
</div>
@@ -92,7 +92,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:page-size="selectedPerPage"
:page-size-options="perPageOptionsList"
:show-global-filter="false"
footer-title="Source Maps"
:footer-title="t('rum.sourceMaps')"
expansion="single"
expand-on-row-click
v-model:expanded-ids="expandedIds"
@@ -135,7 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="`source-maps-${row.service}-delete`"
variant="ghost-destructive"
size="icon-sm"
title="Delete"
:title="t('common.delete')"
@click="confirmDeleteSourceMap(row)"
>
<OIcon name="delete" size="sm" />
@@ -160,8 +160,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
size="xs"
:title="deleteDialog.title"
data-test="delete-source-maps-dialog"
secondary-button-label="Cancel"
primary-button-label="OK"
:secondary-button-label="t('common.cancel')"
:primary-button-label="t('common.ok')"
@click:secondary="deleteDialog.show = false"
@click:primary="deleteSourceMap(); deleteDialog.show = false"
>
@@ -173,6 +173,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import sourcemapsService from "@/services/sourcemaps";
@@ -186,6 +187,7 @@ import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import type { OTableColumnDef } from "@/lib/core/Table/OTable.types";
import { toast } from "@/lib/feedback/Toast/useToast";
const { t } = useI18n();
const store = useStore();
const router = useRouter();
@@ -308,38 +310,38 @@ const groupedSourceMaps = ref<any[]>([]);
const expandedIds = ref<string[]>([]);
// Table columns
const columns: OTableColumnDef[] = [
const columns = computed<OTableColumnDef[]>(() => [
{
id: "service",
header: "Service",
header: t("rum.service"),
accessorKey: "service",
sortable: true,
meta: { align: "left" },
},
{
id: "version",
header: "Version",
header: t("common.version"),
accessorKey: "version",
sortable: true,
meta: { align: "left" },
},
{
id: "environment",
header: "Environment",
header: t("rum.environment"),
accessorKey: "env",
sortable: true,
meta: { align: "left" },
},
{
id: "file_count",
header: "Files",
header: t("rum.files"),
accessorKey: "fileCount",
sortable: true,
meta: { align: "left" },
},
{
id: "uploaded_at",
header: "Uploaded At",
header: t("rum.uploadedAt"),
accessorKey: "uploaded_at",
sortable: true,
meta: {
@@ -349,13 +351,13 @@ const columns: OTableColumnDef[] = [
},
{
id: "actions",
header: "Actions",
header: t("common.actions"),
accessorKey: "actions",
meta: { align: "center", actionCount: 1 },
isAction: true,
size: 80,
},
];
]);
// Pagination
const selectedPerPage = ref<number>(20);