ui: align community agent and settings copy
This commit is contained in:
@@ -2,55 +2,9 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/community-surface-trim`
|
||||
|
||||
Status: in_progress
|
||||
|
||||
Goal:
|
||||
- привести открытую редакцию к честному product surface и убрать расхождение между текущим кодом и edition policy.
|
||||
|
||||
Main code areas:
|
||||
- `apps/ui/js/wizard.js`
|
||||
- `apps/ui/js/wizard-model.js`
|
||||
- `apps/ui/js/agents.js`
|
||||
- `apps/ui/js/settings.js`
|
||||
- `apps/ui/js/i18n.js`
|
||||
- `docs/product-editions.md`
|
||||
- `docs/architecture.md`
|
||||
- `docs/mcp-interface.md`
|
||||
|
||||
Implementation slices:
|
||||
1. Скрыть или честно заблокировать:
|
||||
- `WebSocket`
|
||||
- `SOAP`
|
||||
- `gRPC streaming`
|
||||
- streaming execution modes
|
||||
- `elevated/strict` security levels
|
||||
в Community.
|
||||
2. Привести copy и feature affordances к open-core границе.
|
||||
3. Добавить edition-aware explanation в wizard и agents UI.
|
||||
|
||||
Progress:
|
||||
- done: Community wizard protocol list hides premium protocol cards;
|
||||
- done: Community security note is shown inside wizard flow;
|
||||
- done: server-side Community enforcement rejects unsupported protocols and premium security levels;
|
||||
- done: Community protocol capabilities and wizard surface no longer present streaming execution as available;
|
||||
- pending: привести Agents/Settings copy к окончательной open-core границе.
|
||||
|
||||
DoD:
|
||||
- Community UX не создает ложного ожидания наличия premium functionality;
|
||||
- protocol set и security levels совпадают в docs, UI и runtime contracts.
|
||||
|
||||
Verification:
|
||||
- e2e checks for wizard protocol list;
|
||||
- manual Community smoke;
|
||||
- localized copy review.
|
||||
|
||||
## Next
|
||||
|
||||
### `feat/private-token-service-seam`
|
||||
|
||||
Status: ready
|
||||
Status: in_progress
|
||||
|
||||
Goal:
|
||||
- подготовить public codebase к private реализации короткоживущих и одноразовых токенов.
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-heading" data-i18n="agents.title">Agents</h1>
|
||||
<p class="page-subheading" data-i18n="agents.subtitle">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
|
||||
<p class="page-subheading" x-text="subtitleText()">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
|
||||
</div>
|
||||
<button class="btn-new" @click="openCreate()">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
@@ -147,7 +147,7 @@
|
||||
<!-- Info callout -->
|
||||
<div class="agents-info-callout">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="7"/><path d="M8 11V8M8 5v-.5"/></svg>
|
||||
<span x-text="tfKey('agents.callout', { count: operations.length })">Each agent gets its own MCP endpoint.</span>
|
||||
<span x-text="agentCalloutText()">Each agent gets its own MCP endpoint.</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
@@ -167,7 +167,7 @@
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.35"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 17.5h7M17.5 14v7"/></svg>
|
||||
</div>
|
||||
<div class="empty-state-title" data-i18n="agents.empty.initial.title">No agents yet</div>
|
||||
<div class="empty-state-sub" data-i18n="agents.empty.initial.sub">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
|
||||
<div class="empty-state-sub" x-text="emptyStateText()">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
|
||||
<button class="btn-new" @click="openCreate()" style="margin-top: 16px;">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="agents.new">New agent</span>
|
||||
@@ -269,7 +269,7 @@
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
<div class="drawer-title" x-text="drawerMode === 'create' ? tKey('agents.drawer.new_title') : tKey('agents.drawer.edit_title')"></div>
|
||||
<div class="drawer-subtitle" x-text="drawerMode === 'create' ? tKey('agents.drawer.new_sub') : tKey('agents.drawer.edit_sub')"></div>
|
||||
<div class="drawer-subtitle" x-text="drawerSubText()"></div>
|
||||
</div>
|
||||
<button class="drawer-close" @click="closeDrawer()">
|
||||
<svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
@@ -326,7 +326,7 @@
|
||||
<div class="drawer-section-title" data-i18n="agents.drawer.operations">Operations</div>
|
||||
<span class="drawer-section-count" x-text="tfKey('agents.drawer.selected', { count: form.selectedOps.length })"></span>
|
||||
</div>
|
||||
<div class="drawer-section-sub" data-i18n="agents.drawer.operations_sub">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
|
||||
<div class="drawer-section-sub" x-text="operationsSubText()">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
|
||||
|
||||
<div class="ops-picker">
|
||||
<!-- Search -->
|
||||
|
||||
@@ -48,6 +48,7 @@ document.addEventListener('alpine:init', function() {
|
||||
return {
|
||||
agents: [],
|
||||
operations: [],
|
||||
capabilities: null,
|
||||
loading: true,
|
||||
loadError: '',
|
||||
workspaceId: null,
|
||||
@@ -76,6 +77,7 @@ document.addEventListener('alpine:init', function() {
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
this.workspaceId = workspace ? workspace.id : null;
|
||||
await this.loadCapabilities();
|
||||
await this.reload();
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
@@ -90,10 +92,23 @@ document.addEventListener('alpine:init', function() {
|
||||
|
||||
window.addEventListener('crank:workspacechange', async function(event) {
|
||||
self.workspaceId = event.detail ? event.detail.id : null;
|
||||
await self.loadCapabilities();
|
||||
await self.reload();
|
||||
});
|
||||
},
|
||||
|
||||
async loadCapabilities() {
|
||||
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
||||
this.capabilities = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.capabilities = await window.CrankApi.getCapabilities();
|
||||
} catch (_error) {
|
||||
this.capabilities = null;
|
||||
}
|
||||
},
|
||||
|
||||
async reload() {
|
||||
this.loading = true;
|
||||
this.loadError = '';
|
||||
@@ -150,6 +165,36 @@ document.addEventListener('alpine:init', function() {
|
||||
}, 0);
|
||||
},
|
||||
|
||||
get isCommunityBuild() {
|
||||
return !!(this.capabilities && this.capabilities.edition === 'community');
|
||||
},
|
||||
|
||||
agentCalloutText() {
|
||||
if (this.isCommunityBuild) {
|
||||
return this.tfKey('agents.callout.community', { count: this.operations.length });
|
||||
}
|
||||
return this.tfKey('agents.callout.default', { count: this.operations.length });
|
||||
},
|
||||
|
||||
subtitleText() {
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.subtitle.community' : 'agents.subtitle');
|
||||
},
|
||||
|
||||
emptyStateText() {
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.empty.initial.sub_community' : 'agents.empty.initial.sub');
|
||||
},
|
||||
|
||||
drawerSubText() {
|
||||
if (this.drawerMode === 'edit') {
|
||||
return this.tKey('agents.drawer.edit_sub');
|
||||
}
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.drawer.new_sub_community' : 'agents.drawer.new_sub');
|
||||
},
|
||||
|
||||
operationsSubText() {
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.drawer.operations_sub_community' : 'agents.drawer.operations_sub');
|
||||
},
|
||||
|
||||
get filteredOps() {
|
||||
var query = this.opSearch.toLowerCase().trim();
|
||||
if (!query) return this.operations;
|
||||
|
||||
+24
-14
@@ -378,7 +378,7 @@ var TRANSLATIONS = {
|
||||
'settings.profile.email': 'Email',
|
||||
'settings.profile.role': 'Role',
|
||||
'settings.page.title': 'Account settings',
|
||||
'settings.page.subtitle': 'Manage your profile, password, and workspace settings.',
|
||||
'settings.page.subtitle': 'Manage your profile, password, workspace settings, and the capability limits of this build.',
|
||||
'settings.ws.slug_hint': 'Used in API endpoints and across the console. Only lowercase letters, numbers and hyphens.',
|
||||
'settings.ws.description_optional': 'Description (optional)',
|
||||
'settings.ws.description_placeholder': 'What does this workspace do?',
|
||||
@@ -403,9 +403,9 @@ var TRANSLATIONS = {
|
||||
'settings.security.mismatch': 'New password and confirmation do not match.',
|
||||
'settings.security.saved': 'Password updated.',
|
||||
'settings.security.save_error': 'Failed to change password',
|
||||
'settings.security.capabilities_title': 'More security options',
|
||||
'settings.security.capabilities_body': 'Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.',
|
||||
'settings.security.not_available': 'Available later.',
|
||||
'settings.security.capabilities_title': 'Build capability summary',
|
||||
'settings.security.capabilities_body': 'This section shows what the current build supports. Password sign-in with one HttpOnly browser session is available now; short-lived tokens, one-time tokens, and advanced governance depend on the edition.',
|
||||
'settings.security.not_available': 'Build capabilities',
|
||||
'settings.security.capability_title_community': 'Community build',
|
||||
'settings.security.capability_title_enterprise': 'Enterprise build',
|
||||
'settings.security.capability_title_cloud': 'Cloud build',
|
||||
@@ -431,8 +431,8 @@ var TRANSLATIONS = {
|
||||
'settings.session.current_workspace': 'current workspace',
|
||||
'settings.session.summary': '{email} · {role} in {workspace}. Use Log out to revoke the current browser session.',
|
||||
'settings.notifications.title': 'Notifications',
|
||||
'settings.notifications.not_ready_title': 'Available later.',
|
||||
'settings.notifications.not_ready_body': 'Notification routing and per-user preferences will be added later.',
|
||||
'settings.notifications.not_ready_title': 'Not included in this build.',
|
||||
'settings.notifications.not_ready_body': 'Notification routing and per-user preferences are outside the current Community surface.',
|
||||
'settings.planned_badge': 'Planned',
|
||||
'settings.notifications.spike_title': 'Operation error spike',
|
||||
'settings.notifications.spike_body': 'Alert when error rate exceeds a rolling threshold for a published tool.',
|
||||
@@ -936,6 +936,7 @@ var TRANSLATIONS = {
|
||||
// Agents page
|
||||
'agents.title': 'Agents',
|
||||
'agents.subtitle': 'Named MCP endpoints that expose a curated subset of operations to an LLM',
|
||||
'agents.subtitle.community': 'Dedicated MCP endpoints for AI agents. Community keeps machine access on static agent keys.',
|
||||
'agents.new': 'New agent',
|
||||
'agents.limit.title': 'Agent limit reached',
|
||||
'agents.limit.message': 'This Community build allows up to {limit} AI agent. Upgrade the edition or remove an existing agent to add another one.',
|
||||
@@ -949,13 +950,15 @@ var TRANSLATIONS = {
|
||||
'agents.stats.calls_sub': 'all agents combined',
|
||||
'agents.search': 'Search agents...',
|
||||
'agents.results': '{shown} of {total} agents',
|
||||
'agents.callout': 'Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.',
|
||||
'agents.callout.default': 'Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.',
|
||||
'agents.callout.community': 'Each agent gets its own MCP endpoint and static agent keys in Community. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.',
|
||||
'agents.loading': 'Loading agents…',
|
||||
'agents.error.title': 'Failed to load agents',
|
||||
'agents.error.api': 'Workspace or API is unavailable',
|
||||
'agents.error.load': 'Failed to load agents',
|
||||
'agents.empty.initial.title': 'No agents yet',
|
||||
'agents.empty.initial.sub': 'Create your first agent to get a dedicated MCP endpoint with a curated set of tools.',
|
||||
'agents.empty.initial.sub_community': 'Create your first agent to get one dedicated MCP endpoint and its static machine-access keys for a focused toolset.',
|
||||
'agents.empty.search.title': 'No agents match "{query}"',
|
||||
'agents.empty.search.sub': 'Try a different search term.',
|
||||
'agents.card.tools': 'tools',
|
||||
@@ -975,6 +978,7 @@ var TRANSLATIONS = {
|
||||
'agents.drawer.new_title': 'New agent',
|
||||
'agents.drawer.edit_title': 'Edit agent',
|
||||
'agents.drawer.new_sub': 'Configure a named MCP endpoint for your LLM',
|
||||
'agents.drawer.new_sub_community': 'Configure one MCP endpoint and its static agent-key boundary for a focused LLM use case',
|
||||
'agents.drawer.edit_sub': 'Update agent settings and tool selection',
|
||||
'agents.drawer.identity': 'Identity',
|
||||
'agents.drawer.display_name': 'Display name',
|
||||
@@ -990,6 +994,7 @@ var TRANSLATIONS = {
|
||||
'agents.drawer.operations': 'Operations',
|
||||
'agents.drawer.selected': '{count} selected',
|
||||
'agents.drawer.operations_sub': 'Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.',
|
||||
'agents.drawer.operations_sub_community': 'Select which operations this agent exposes. Any keys issued for this agent only work against this MCP endpoint.',
|
||||
'agents.drawer.filter_ops': 'Filter operations…',
|
||||
'agents.drawer.ops_no_match': 'No operations match "{query}"',
|
||||
'agents.drawer.ops_selected': '{count} operations selected',
|
||||
@@ -1430,7 +1435,7 @@ var TRANSLATIONS = {
|
||||
'settings.profile.email': 'Email',
|
||||
'settings.profile.role': 'Роль',
|
||||
'settings.page.title': 'Настройки аккаунта',
|
||||
'settings.page.subtitle': 'Управляйте профилем, паролем и настройками воркспейса.',
|
||||
'settings.page.subtitle': 'Управляйте профилем, паролем, настройками воркспейса и границами возможностей текущей сборки.',
|
||||
'settings.ws.slug_hint': 'Используется в API endpoint-ах и по всей консоли. Только строчные буквы, цифры и дефисы.',
|
||||
'settings.ws.description_optional': 'Описание (необязательно)',
|
||||
'settings.ws.description_placeholder': 'Чем занимается этот воркспейс?',
|
||||
@@ -1455,9 +1460,9 @@ var TRANSLATIONS = {
|
||||
'settings.security.mismatch': 'Новый пароль и подтверждение не совпадают.',
|
||||
'settings.security.saved': 'Пароль обновлен.',
|
||||
'settings.security.save_error': 'Не удалось изменить пароль',
|
||||
'settings.security.capabilities_title': 'Дополнительные параметры безопасности',
|
||||
'settings.security.capabilities_body': 'Двухфакторная аутентификация, passkeys и управление несколькими сессиями появятся позже. Сейчас доступен вход по паролю с одной HttpOnly browser session.',
|
||||
'settings.security.not_available': 'Появится позже.',
|
||||
'settings.security.capabilities_title': 'Сводка по возможностям сборки',
|
||||
'settings.security.capabilities_body': 'Этот блок показывает, что поддерживает текущая сборка. Сейчас доступен вход по паролю с одной HttpOnly browser session; короткоживущие токены, одноразовые токены и расширенное управление доступом зависят от редакции.',
|
||||
'settings.security.not_available': 'Возможности сборки',
|
||||
'settings.security.capability_title_community': 'Редакция Community',
|
||||
'settings.security.capability_title_enterprise': 'Редакция Enterprise',
|
||||
'settings.security.capability_title_cloud': 'Редакция Cloud',
|
||||
@@ -1483,8 +1488,8 @@ var TRANSLATIONS = {
|
||||
'settings.session.current_workspace': 'текущий воркспейс',
|
||||
'settings.session.summary': '{email} · {role} в {workspace}. Используйте «Выйти», чтобы отозвать текущую browser session.',
|
||||
'settings.notifications.title': 'Уведомления',
|
||||
'settings.notifications.not_ready_title': 'Появятся позже.',
|
||||
'settings.notifications.not_ready_body': 'Маршрутизация уведомлений и персональные настройки появятся позже.',
|
||||
'settings.notifications.not_ready_title': 'Не входит в эту сборку.',
|
||||
'settings.notifications.not_ready_body': 'Маршрутизация уведомлений и персональные настройки находятся вне текущей поверхности Community.',
|
||||
'settings.planned_badge': 'Запланировано',
|
||||
'settings.notifications.spike_title': 'Всплеск ошибок операции',
|
||||
'settings.notifications.spike_body': 'Алерт, когда доля ошибок превышает скользящий порог для опубликованного инструмента.',
|
||||
@@ -1998,6 +2003,7 @@ var TRANSLATIONS = {
|
||||
// Agents page
|
||||
'agents.title': 'Агенты',
|
||||
'agents.subtitle': 'Именованные MCP endpoint-ы, открывающие LLM ограниченный набор операций',
|
||||
'agents.subtitle.community': 'Отдельные MCP endpoint-ы для AI-агентов. В Community машинный доступ держится на статических ключах агента.',
|
||||
'agents.new': 'Новый агент',
|
||||
'agents.limit.title': 'Достигнут лимит агентов',
|
||||
'agents.limit.message': 'В этой Community-сборке доступно не более {limit} AI-агента. Чтобы добавить еще одного, удалите существующего агента или перейдите на другую редакцию.',
|
||||
@@ -2011,13 +2017,15 @@ var TRANSLATIONS = {
|
||||
'agents.stats.calls_sub': 'по всем агентам',
|
||||
'agents.search': 'Поиск агентов...',
|
||||
'agents.results': '{shown} из {total} агентов',
|
||||
'agents.callout': 'У каждого агента свой MCP endpoint. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.',
|
||||
'agents.callout.default': 'У каждого агента свой MCP endpoint. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.',
|
||||
'agents.callout.community': 'В Community у каждого агента свой MCP endpoint и свои статические ключи агента. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.',
|
||||
'agents.loading': 'Загрузка агентов…',
|
||||
'agents.error.title': 'Не удалось загрузить агентов',
|
||||
'agents.error.api': 'Воркспейс или API недоступен',
|
||||
'agents.error.load': 'Не удалось загрузить агентов',
|
||||
'agents.empty.initial.title': 'Агентов пока нет',
|
||||
'agents.empty.initial.sub': 'Создайте первого агента, чтобы получить отдельный MCP endpoint с нужным набором инструментов.',
|
||||
'agents.empty.initial.sub_community': 'Создайте первого агента, чтобы получить один отдельный MCP endpoint и его статические машинные ключи для конкретного набора инструментов.',
|
||||
'agents.empty.search.title': 'Нет агентов по запросу "{query}"',
|
||||
'agents.empty.search.sub': 'Попробуйте другой поисковый запрос.',
|
||||
'agents.card.tools': 'инструментов',
|
||||
@@ -2037,6 +2045,7 @@ var TRANSLATIONS = {
|
||||
'agents.drawer.new_title': 'Новый агент',
|
||||
'agents.drawer.edit_title': 'Редактировать агента',
|
||||
'agents.drawer.new_sub': 'Настройте именованный MCP endpoint для вашей LLM',
|
||||
'agents.drawer.new_sub_community': 'Настройте один MCP endpoint и его границу на статических ключах агента для конкретного LLM-сценария',
|
||||
'agents.drawer.edit_sub': 'Обновите настройки агента и выбор инструментов',
|
||||
'agents.drawer.identity': 'Идентичность',
|
||||
'agents.drawer.display_name': 'Отображаемое имя',
|
||||
@@ -2052,6 +2061,7 @@ var TRANSLATIONS = {
|
||||
'agents.drawer.operations': 'Операции',
|
||||
'agents.drawer.selected': 'Выбрано: {count}',
|
||||
'agents.drawer.operations_sub': 'Выберите операции, которые открывает этот агент. LLM, подключенная к агенту, увидит только эти инструменты.',
|
||||
'agents.drawer.operations_sub_community': 'Выберите операции, которые открывает этот агент. Любые ключи, выпущенные для этого агента, будут работать только против этого MCP endpoint.',
|
||||
'agents.drawer.filter_ops': 'Фильтр операций…',
|
||||
'agents.drawer.ops_no_match': 'Нет операций по запросу "{query}"',
|
||||
'agents.drawer.ops_selected': 'Выбрано операций: {count}',
|
||||
|
||||
@@ -5,7 +5,9 @@ test('agents page shows demo cards and edit drawer opens', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/agents');
|
||||
await expect(page.locator('.page-heading')).toHaveText(localized('Agents', 'Агенты'));
|
||||
await expect(page.locator('.agents-info-callout')).toContainText(localized('static agent keys', 'статические ключи агента'));
|
||||
await page.getByRole('button', { name: localized('New agent', 'Новый агент') }).click();
|
||||
await expect(page.locator('.drawer-title')).toHaveText(localized('New agent', 'Новый агент'));
|
||||
await expect(page.locator('.drawer-subtitle')).toContainText(localized('static agent-key boundary', 'статических ключах агента'));
|
||||
await expect(page.locator('.drawer')).toContainText(/mcp/i);
|
||||
});
|
||||
|
||||
@@ -13,4 +13,6 @@ test('workspace and settings pages show live session data', async ({ page }) =>
|
||||
await expect(page.locator('.page-title')).toHaveText(localized('Account settings', 'Настройки аккаунта'));
|
||||
await expect(page.locator('#settings-session-summary')).toBeVisible();
|
||||
await expect(page.locator('#section-security')).toBeVisible();
|
||||
await expect(page.locator('#settings-security-capability-title')).toContainText(localized('Community build', 'Редакция Community'));
|
||||
await expect(page.locator('#settings-security-capability-body')).toContainText(localized('static agent keys', 'статические ключи агентов'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user