feat: localize operations catalog
This commit is contained in:
+23
-22
@@ -113,7 +113,7 @@
|
||||
</div>
|
||||
<button class="btn-new" @click="handleNewOperation()">
|
||||
<svg width="13" height="13"><use href="icons/general/plus.svg#icon"/></svg>
|
||||
New operation
|
||||
<span data-i18n="ops.new">New operation</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -122,22 +122,22 @@
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="stats.total">Total operations</div>
|
||||
<div class="stat-value" x-text="formatMetricNumber(stats.total)">—</div>
|
||||
<div class="stat-delta" x-text="stats.total ? 'Catalog synced with backend' : 'No operations yet'"></div>
|
||||
<div class="stat-delta" x-text="stats.total ? tKey('ops.stats.synced') : tKey('ops.stats.none')"></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="stats.active">Active</div>
|
||||
<div class="stat-value" x-text="formatMetricNumber(stats.active)">—</div>
|
||||
<div class="stat-delta" x-text="stats.total ? Math.round((stats.active / stats.total) * 100) + '% of total' : 'No published tools yet'">—</div>
|
||||
<div class="stat-delta" x-text="stats.total ? tfKey('ops.stats.share', { value: Math.round((stats.active / stats.total) * 100) }) : tKey('ops.stats.no_published')">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="stats.requests">Requests today</div>
|
||||
<div class="stat-value" x-text="formatMetricNumber(stats.requestsToday)">—</div>
|
||||
<div class="stat-delta" x-text="stats.requestsToday ? 'Summed from operation usage' : 'No traffic recorded today'"></div>
|
||||
<div class="stat-delta" x-text="stats.requestsToday ? tKey('ops.stats.requests_rollup') : tKey('ops.stats.no_traffic')"></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="stats.latency">Avg. latency</div>
|
||||
<div class="stat-value" x-text="stats.averageLatencyMs ? stats.averageLatencyMs + 'ms' : '—'">—</div>
|
||||
<div class="stat-delta" x-text="stats.averageLatencyMs ? `Weighted by today's calls` : 'Waiting for runtime samples'"></div>
|
||||
<div class="stat-delta" x-text="stats.averageLatencyMs ? tKey('ops.stats.latency_weighted') : tKey('ops.stats.latency_waiting')"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
<svg width="14" height="14"><use href="icons/general/search.svg#icon"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
data-i18n-ph="filter.search"
|
||||
placeholder="Search operations..."
|
||||
:value="search"
|
||||
@input="setSearch($event.target.value)"
|
||||
@@ -176,7 +177,7 @@
|
||||
:class="{ 'has-value': filterProtocol, open: openDropdown === 'protocol' }"
|
||||
@click="toggleDropdown('protocol')"
|
||||
>
|
||||
<span x-text="filterProtocol ? filterProtocol.toUpperCase() : 'Protocol'"></span>
|
||||
<span x-text="filterProtocol ? filterProtocol.toUpperCase() : tKey('filter.protocol')"></span>
|
||||
<svg class="chevron" width="10" height="10"><use href="icons/general/chevron-down.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu" x-show="openDropdown === 'protocol'" x-transition>
|
||||
@@ -193,7 +194,7 @@
|
||||
<template x-if="filterProtocol">
|
||||
<div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-clear" @click="clearProtocol()">Clear filter</button>
|
||||
<button class="dropdown-clear" @click="clearProtocol()" data-i18n="filter.clear">Clear filter</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -206,7 +207,7 @@
|
||||
:class="{ 'has-value': filterCategory, open: openDropdown === 'category' }"
|
||||
@click="toggleDropdown('category')"
|
||||
>
|
||||
<span x-text="filterCategory ?? 'Category'"></span>
|
||||
<span x-text="filterCategory ?? tKey('filter.category')"></span>
|
||||
<svg class="chevron" width="10" height="10"><use href="icons/general/chevron-down.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu" x-show="openDropdown === 'category'" x-transition>
|
||||
@@ -223,7 +224,7 @@
|
||||
<template x-if="filterCategory">
|
||||
<div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-clear" @click="clearCategory()">Clear filter</button>
|
||||
<button class="dropdown-clear" @click="clearCategory()" data-i18n="filter.clear">Clear filter</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -236,7 +237,7 @@
|
||||
:class="{ 'has-value': filterAgent, open: openDropdown === 'agent' }"
|
||||
@click="toggleDropdown('agent')"
|
||||
>
|
||||
<span x-text="filterAgent ? activeAgents.find(a => a.agent_id === filterAgent)?.display_name : 'Agent'"></span>
|
||||
<span x-text="filterAgent ? activeAgents.find(a => a.agent_id === filterAgent)?.display_name : tKey('filter.agent')"></span>
|
||||
<svg class="chevron" width="10" height="10"><use href="icons/general/chevron-down.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu" x-show="openDropdown === 'agent'" x-transition>
|
||||
@@ -251,12 +252,12 @@
|
||||
</button>
|
||||
</template>
|
||||
<template x-if="!activeAgents.length">
|
||||
<div class="dropdown-item" style="color:var(--text-muted);cursor:default;">No active agents</div>
|
||||
<div class="dropdown-item" style="color:var(--text-muted);cursor:default;" data-i18n="ops.no_active_agents">No active agents</div>
|
||||
</template>
|
||||
<template x-if="filterAgent">
|
||||
<div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-clear" @click="clearAgent()">Clear filter</button>
|
||||
<button class="dropdown-clear" @click="clearAgent()" data-i18n="filter.clear">Clear filter</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -265,7 +266,7 @@
|
||||
<!-- Right side: count + sort -->
|
||||
<div class="filter-right">
|
||||
<span class="results-count">
|
||||
Showing <span x-text="totalFiltered"></span> of <span x-text="operations.length"></span>
|
||||
<span x-text="tfKey('ops.results', { shown: totalFiltered, total: operations.length })"></span>
|
||||
</span>
|
||||
|
||||
<!-- Sort dropdown -->
|
||||
@@ -300,12 +301,12 @@
|
||||
<template x-for="chip in activeChips" :key="chip.key">
|
||||
<div class="active-filter-chip">
|
||||
<span x-text="chip.label"></span>
|
||||
<button class="active-filter-chip-remove" @click="clearChip(chip.key)" title="Remove filter">
|
||||
<button class="active-filter-chip-remove" @click="clearChip(chip.key)" data-i18n-title="filter.remove" title="Remove filter">
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="7" y2="7"/><line x1="7" y1="1" x2="1" y2="7"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<button class="active-filter-clear-all" @click="clearAllFilters()" x-show="activeChips.length > 1">
|
||||
<button class="active-filter-clear-all" @click="clearAllFilters()" x-show="activeChips.length > 1" data-i18n="filter.clear_all">
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
@@ -320,12 +321,12 @@
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="empty-state" x-show="!loading && !loadError && totalFiltered === 0">
|
||||
<h3 x-text="operations.length === 0 ? 'No operations yet' : 'No operations found'">No operations found</h3>
|
||||
<p x-text="operations.length === 0 ? 'Create the first operation in this workspace to expose a live MCP tool.' : 'Try adjusting your search or filters.'">Try adjusting your search or filters.</p>
|
||||
<h3 x-text="operations.length === 0 ? tKey('ops.empty.initial.title') : tKey('ops.empty.title')">No operations found</h3>
|
||||
<p x-text="operations.length === 0 ? tKey('ops.empty.initial.sub') : tKey('ops.empty.sub')">Try adjusting your search or filters.</p>
|
||||
</div>
|
||||
|
||||
<div class="empty-state" x-show="!loading && !!loadError">
|
||||
<h3>Backend connection failed</h3>
|
||||
<h3 data-i18n="ops.error.title">Backend connection failed</h3>
|
||||
<p x-text="loadError"></p>
|
||||
</div>
|
||||
|
||||
@@ -342,7 +343,7 @@
|
||||
<th data-i18n="th.protocol">PROTOCOL</th>
|
||||
<th data-i18n="th.status">STATUS</th>
|
||||
<th class="th-sortable col-created" :class="{ 'sort-active': sort.startsWith('created') }" @click="setSort(sort === 'created_asc' ? 'created_desc' : 'created_asc')">
|
||||
CREATED
|
||||
<span data-i18n="th.created">CREATED</span>
|
||||
<span class="sort-arrow" :class="{ active: sort.startsWith('created') }">
|
||||
<span x-text="sort === 'created_asc' ? '↑' : sort === 'created_desc' ? '↓' : '↕'"></span>
|
||||
</span>
|
||||
@@ -383,10 +384,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
<button class="row-btn row-btn-edit" title="Edit operation" @click="editOperation(op)">
|
||||
<button class="row-btn row-btn-edit" :title="tKey('ops.action.edit')" @click="editOperation(op)">
|
||||
<svg width="14" height="14"><use href="icons/general/edit.svg#icon"/></svg>
|
||||
</button>
|
||||
<button class="row-btn row-btn-delete" title="Delete operation" @click="deleteOperation(op.id)">
|
||||
<button class="row-btn row-btn-delete" :title="tKey('ops.action.delete')" @click="deleteOperation(op.id)">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -399,7 +400,7 @@
|
||||
<!-- Footer / pagination -->
|
||||
<div class="table-footer" x-show="!loading && totalFiltered > 0 && !loadError">
|
||||
<span class="table-footer-info">
|
||||
Showing <span x-text="pageStart"></span>–<span x-text="pageEnd"></span> of <span x-text="totalFiltered"></span> operations
|
||||
<span x-text="tfKey('ops.range', { from: pageStart, to: pageEnd, total: totalFiltered })"></span>
|
||||
</span>
|
||||
<div class="pagination">
|
||||
<button class="page-btn" :disabled="page === 1" @click="goPage(page - 1)">←</button>
|
||||
|
||||
Reference in New Issue
Block a user