feat: localize remaining alpine ui views
This commit is contained in:
+10
-2
@@ -8,6 +8,14 @@
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
function roleLabel(role) {
|
||||
var normalized = String(role || 'viewer').toLowerCase();
|
||||
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
|
||||
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
|
||||
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
|
||||
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
|
||||
return normalized.replace(/^\w/, function(char) { return char.toUpperCase(); });
|
||||
}
|
||||
|
||||
function currentWorkspaceLabel() {
|
||||
if (window.getCurrentWorkspace) {
|
||||
@@ -86,7 +94,7 @@
|
||||
id: session.user.id,
|
||||
name: session.user.display_name,
|
||||
email: session.user.email,
|
||||
role: membership ? String(membership.role || '').replace(/^\w/, function(char) { return char.toUpperCase(); }) : tKey('workspace_setup.role.viewer'),
|
||||
role: membership ? roleLabel(membership.role) : tKey('workspace_setup.role.viewer'),
|
||||
workspace: membership ? membership.workspace.slug : '',
|
||||
workspaceId: membership ? membership.workspace.id : '',
|
||||
initials: initials(session.user.display_name, session.user.email),
|
||||
@@ -115,7 +123,7 @@
|
||||
? session.user.email
|
||||
: fallbackUser && fallbackUser.email;
|
||||
var role = membership
|
||||
? String(membership.role || '').replace(/^\w/, function(char) { return char.toUpperCase(); })
|
||||
? roleLabel(membership.role)
|
||||
: (fallbackUser && fallbackUser.role) || tKey('workspace_setup.role.viewer');
|
||||
var workspace = membership && membership.workspace
|
||||
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
|
||||
|
||||
+312
-6
@@ -16,6 +16,10 @@ var TRANSLATIONS = {
|
||||
'nav.workspaces': 'Your workspaces',
|
||||
'nav.create_workspace': 'Create workspace',
|
||||
'nav.notifications': 'Notifications',
|
||||
'nav.menu': 'Menu',
|
||||
'nav.account': 'Account',
|
||||
'nav.user_fallback': 'Crank',
|
||||
'nav.workspace_fallback': 'workspace',
|
||||
|
||||
// Operations page
|
||||
'ops.title': 'Operations',
|
||||
@@ -302,9 +306,11 @@ var TRANSLATIONS = {
|
||||
'settings.security.capabilities_body': 'Two-factor authentication, passkeys and multi-session inventory are planned, but the current live flow is password-based login with one HttpOnly browser session.',
|
||||
'settings.security.not_available': 'Not available in this build.',
|
||||
'settings.session.title': 'Current session',
|
||||
'settings.session.browser': 'Browser session',
|
||||
'settings.session.current': 'current',
|
||||
'settings.session.loading': 'Loading current session details…',
|
||||
'settings.session.unavailable': 'Session details are unavailable. Use Log out to revoke the current browser session.',
|
||||
'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': 'Notification preferences',
|
||||
'settings.notifications.not_ready_title': 'Notifications are not wired yet.',
|
||||
@@ -411,6 +417,7 @@ var TRANSLATIONS = {
|
||||
'wizard.progress.edit': 'Edit operation',
|
||||
'wizard.exit': 'Exit wizard',
|
||||
'wizard.step_of': 'Step <strong>{step}</strong> of {total}',
|
||||
'wizard.step_short': 'Step {step}',
|
||||
'wizard.status.completed': 'Completed',
|
||||
'wizard.status.in_progress': 'In progress',
|
||||
'wizard.status.not_started': 'Not started',
|
||||
@@ -431,23 +438,167 @@ var TRANSLATIONS = {
|
||||
'wizard.sidebar.brand.edit': 'Edit <span>operation</span>',
|
||||
'wizard.step1.title': 'Choose a protocol',
|
||||
'wizard.step1.subtitle': 'Select the transport protocol your upstream service uses. This determines how Crank will communicate with your API and which configuration options are available in subsequent steps.',
|
||||
'wizard.step1.rest_name': 'REST / HTTP',
|
||||
'wizard.step1.rest_tagline': 'Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.',
|
||||
'wizard.step1.graphql_name': 'GraphQL',
|
||||
'wizard.step1.graphql_tagline': 'Query and mutate a GraphQL schema. Supports queries and mutations over one fixed operation.',
|
||||
'wizard.step1.grpc_name': 'gRPC',
|
||||
'wizard.step1.grpc_tagline': 'High-performance binary RPC over HTTP/2. Ideal for low-latency internal services.',
|
||||
'wizard.step1.query': 'Query',
|
||||
'wizard.step1.mutation': 'Mutation',
|
||||
'wizard.step1.unary': 'Unary',
|
||||
'wizard.step1.tip_title': 'You can change this later',
|
||||
'wizard.step1.tip_body': 'Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.',
|
||||
'wizard.step2.title': 'Select upstream & endpoint',
|
||||
'wizard.step2.subtitle': 'Choose an existing upstream or register a new one. Then define the specific endpoint path this operation will call.',
|
||||
'wizard.required': 'Required',
|
||||
'wizard.optional': 'Optional',
|
||||
'wizard.step2.upstream_title': 'Upstream',
|
||||
'wizard.step2.upstream_subtitle': 'Shared host, base URL, and authentication',
|
||||
'wizard.step2.upstream_placeholder': 'Select an upstream…',
|
||||
'wizard.step2.search_placeholder': 'Search by name or URL…',
|
||||
'wizard.step2.change': 'Change',
|
||||
'wizard.step2.register_new': 'Register new upstream',
|
||||
'wizard.step2.name': 'Name',
|
||||
'wizard.step2.base_url': 'Base URL',
|
||||
'wizard.step2.unique_hint': 'Unique identifier for this upstream.',
|
||||
'wizard.step2.base_url_hint': 'Root URL — no trailing slash. Supports ${secrets.*}.',
|
||||
'wizard.step2.auth_headers': 'Auth headers',
|
||||
'wizard.step2.auth_headers_hint': 'These headers are sent on every request to this upstream. Use secrets for credentials.',
|
||||
'wizard.step2.save_upstream': 'Save upstream',
|
||||
'wizard.step2.endpoint_title': 'Endpoint',
|
||||
'wizard.step2.endpoint_subtitle': 'Path and HTTP method for this specific operation',
|
||||
'wizard.step2.path_template': 'Path template',
|
||||
'wizard.step2.path_hint': 'Appended to the upstream base URL. Use {param} for path variables.',
|
||||
'wizard.step3.rest.label': 'Request config',
|
||||
'wizard.step3.graphql.label': 'GQL operation',
|
||||
'wizard.step3.grpc.label': 'RPC method',
|
||||
'wizard.step3.rest.title': 'HTTP method & format',
|
||||
'wizard.step3.rest.subtitle': 'Choose the HTTP verb this operation sends and configure content negotiation headers. Crank will serialize MCP tool arguments into the appropriate request body format.',
|
||||
'wizard.step3.rest.method_title': 'HTTP method',
|
||||
'wizard.step3.rest.method_subtitle': 'Verb sent to the upstream on every tool invocation',
|
||||
'wizard.step3.rest.read': 'Read',
|
||||
'wizard.step3.rest.create': 'Create',
|
||||
'wizard.step3.rest.replace': 'Replace',
|
||||
'wizard.step3.rest.update': 'Update',
|
||||
'wizard.step3.rest.remove': 'Remove',
|
||||
'wizard.step3.rest.format_title': 'Request format',
|
||||
'wizard.step3.rest.format_subtitle': 'Content negotiation and serialisation',
|
||||
'wizard.step3.rest.content_type': 'Content-Type',
|
||||
'wizard.step3.rest.content_type_hint': 'How the request body is encoded when sent to the upstream.',
|
||||
'wizard.step3.rest.accept': 'Accept',
|
||||
'wizard.step3.rest.accept_hint': 'Accepted response content types from the upstream server.',
|
||||
'wizard.step3.graphql.title': 'GraphQL operation',
|
||||
'wizard.step3.graphql.subtitle': 'Define the GraphQL operation this tool will execute. All requests are sent as HTTP POST to the endpoint configured in the previous step. Variables are populated from MCP tool arguments at runtime.',
|
||||
'wizard.step3.graphql.type_title': 'Operation type',
|
||||
'wizard.step3.graphql.type_subtitle': 'GraphQL operation kind — subscriptions are not supported',
|
||||
'wizard.step3.graphql.query_desc': 'Fetch data without side-effects',
|
||||
'wizard.step3.graphql.mutation_desc': 'Create, update or delete data',
|
||||
'wizard.step3.graphql.doc_title': 'Query document',
|
||||
'wizard.step3.graphql.doc_subtitle': 'GraphQL document with typed variable declarations',
|
||||
'wizard.step3.graphql.variables_title': 'Variables are mapped in step 5',
|
||||
'wizard.step3.graphql.variables_body': 'Declare all variables in the query document using the $variable: Type syntax. In step 5 you will map MCP input fields to these variables. Crank serializes the final {"query","variables"} payload automatically.',
|
||||
'wizard.step3.grpc.title': 'RPC method',
|
||||
'wizard.step3.grpc.subtitle': 'Choose how to describe the gRPC service contract. Upload a .proto file, discover methods from a live server via reflection, or enter the route manually. Only unary RPCs are supported.',
|
||||
'wizard.step3.grpc.source_proto': 'Proto file',
|
||||
'wizard.step3.grpc.source_reflection': 'Descriptor set',
|
||||
'wizard.step3.grpc.source_manual': 'Manual',
|
||||
'wizard.step3.grpc.proto_title': 'Proto definition',
|
||||
'wizard.step3.grpc.proto_subtitle': 'Upload or paste your .proto file',
|
||||
'wizard.step3.grpc.drop_title': 'Drop .proto file here',
|
||||
'wizard.step3.grpc.drop_subtitle': 'or click to browse',
|
||||
'wizard.step3.grpc.change_file': 'Change file',
|
||||
'wizard.step3.grpc.paste_content': 'Paste content',
|
||||
'wizard.step3.grpc.parse_proto': 'Parse proto',
|
||||
'wizard.step3.grpc.streaming_excluded': 'Streaming methods excluded',
|
||||
'wizard.step3.grpc.streaming_hidden': '{count} streaming method(s) found and hidden — not compatible with MCP tool calls.',
|
||||
'wizard.step3.grpc.select_method': 'Select method',
|
||||
'wizard.step3.grpc.discovery_live_title': 'Descriptor-driven discovery',
|
||||
'wizard.step3.grpc.discovery_live_subtitle': 'Use the saved draft and a descriptor set to discover services through the backend',
|
||||
'wizard.step3.grpc.no_upstream': 'No upstream selected',
|
||||
'wizard.step3.grpc.select_upstream_first': 'Select an upstream in step 2 first',
|
||||
'wizard.step3.grpc.open_tools': 'Open live descriptor tools',
|
||||
'wizard.step3.grpc.connecting': 'Connecting…',
|
||||
'wizard.step3.grpc.live_step5': 'Live descriptor discovery is handled in step 5.',
|
||||
'wizard.step3.grpc.retry': 'Retry',
|
||||
'wizard.step3.grpc.manual_title': 'Method route',
|
||||
'wizard.step3.grpc.manual_subtitle': 'Full gRPC path to the unary method',
|
||||
'wizard.step3.grpc.route': 'gRPC route',
|
||||
'wizard.step3.grpc.route_hint': 'Format: /[package.]ServiceName/MethodName',
|
||||
'wizard.step3.grpc.request_type': 'Request type',
|
||||
'wizard.step3.grpc.response_type': 'Response type',
|
||||
'wizard.step3.grpc.proto_doc_hint': 'Protobuf message name, for documentation.',
|
||||
'wizard.step3.grpc.schema_title': 'Input and output schemas are defined in step 4',
|
||||
'wizard.step3.grpc.schema_body': 'In manual mode you only need the route. The MCP tool input/output schemas — which the LLM uses to call this tool — are configured in the next step regardless of Protobuf definitions.',
|
||||
'wizard.step3.grpc.signature_title': 'Method signature',
|
||||
'wizard.step3.grpc.route_label': 'gRPC route',
|
||||
'wizard.step3.grpc.request_label': 'Request',
|
||||
'wizard.step3.grpc.response_label': 'Response',
|
||||
'wizard.step4.title': 'Tool config',
|
||||
'wizard.step4.subtitle': 'Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.',
|
||||
'wizard.step4.identity_title': 'Tool identity',
|
||||
'wizard.step4.identity_subtitle': 'Machine-readable name and LLM-facing description',
|
||||
'wizard.step4.tool_name': 'Tool name',
|
||||
'wizard.step4.tool_name_hint': 'Machine-readable identifier used in MCP tool calls. Only lowercase letters, numbers and underscores. Cannot be changed after publishing.',
|
||||
'wizard.step4.display_name': 'Display name',
|
||||
'wizard.step4.display_name_hint': 'Human-readable name shown in the console and audit log.',
|
||||
'wizard.step4.tool_title': 'Tool title',
|
||||
'wizard.step4.tool_title_hint': 'Short imperative sentence shown in the MCP tool manifest.',
|
||||
'wizard.step4.description': 'Description',
|
||||
'wizard.step4.description_hint': 'LLM-facing description. Be precise — this is the primary signal the model uses to decide whether to invoke this tool.',
|
||||
'wizard.step4.description_title': 'Writing effective descriptions',
|
||||
'wizard.step4.description_body': 'Start with a verb ("Creates", "Fetches", "Updates"). Mention key input requirements. Describe what a successful response looks like. Avoid vague phrasing like "handles" or "manages". See description best practices →',
|
||||
'wizard.step4.input_schema_title': 'Input schema',
|
||||
'wizard.step4.input_schema_subtitle': 'Parameters the LLM passes when calling this tool',
|
||||
'wizard.step4.output_schema_title': 'Output schema',
|
||||
'wizard.step4.output_schema_subtitle': 'Shape of the data returned to the LLM after a successful call',
|
||||
'wizard.step4.schema_draft': 'JSON Schema draft-07',
|
||||
'wizard.step4.protocol_agnostic_title': 'Schemas are protocol-agnostic',
|
||||
'wizard.step4.protocol_agnostic_body': 'These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.',
|
||||
'wizard.step5.title': 'Mapping and execution',
|
||||
'wizard.step5.subtitle': 'Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 3.',
|
||||
'wizard.step5.input_request': 'Input → Request',
|
||||
'wizard.step5.output_response': 'Response → Output',
|
||||
'wizard.step5.execution': 'Execution',
|
||||
'wizard.step5.exec_title': 'Execution config',
|
||||
'wizard.step5.exec_subtitle': 'Timeouts, retry policy and auth profile reference',
|
||||
'wizard.step5.live_title': 'Live validation and publishing',
|
||||
'wizard.step5.draft_actions': 'Draft actions',
|
||||
'wizard.step5.live_save_title': 'Live actions save the draft first',
|
||||
'wizard.step5.live_save_body': 'Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.',
|
||||
'wizard.step5.samples_title': 'Samples and draft generation',
|
||||
'wizard.step5.samples_subtitle': 'Persist input/output samples, then generate schemas and mappings from real payloads.',
|
||||
'wizard.step5.input_sample': 'Input sample',
|
||||
'wizard.step5.output_sample': 'Output sample',
|
||||
'wizard.step5.save_input_sample': 'Save input sample',
|
||||
'wizard.step5.save_output_sample': 'Save output sample',
|
||||
'wizard.step5.generate_draft': 'Generate draft from samples',
|
||||
'wizard.step5.test_title': 'Test run',
|
||||
'wizard.step5.test_subtitle': 'Run the current draft against the upstream before publishing it.',
|
||||
'wizard.step5.test_input_payload': 'Test input payload',
|
||||
'wizard.step5.run_test': 'Run test',
|
||||
'wizard.step5.use_response_output': 'Use response as output sample',
|
||||
'wizard.step5.request_preview': 'Request preview',
|
||||
'wizard.step5.response_preview': 'Response preview',
|
||||
'wizard.step5.errors': 'Errors',
|
||||
'wizard.step5.grpc_descriptor_title': 'gRPC descriptor set',
|
||||
'wizard.step5.grpc_descriptor_subtitle': 'Upload a compiled descriptor set and discover unary methods from the saved draft.',
|
||||
'wizard.step5.choose_descriptor': 'Choose descriptor set',
|
||||
'wizard.step5.upload_discover': 'Upload and discover services',
|
||||
'wizard.step5.no_descriptor': 'No descriptor set selected',
|
||||
'wizard.step5.yaml_title': 'YAML import and export',
|
||||
'wizard.step5.yaml_subtitle': 'Move operation drafts between environments without leaving the wizard.',
|
||||
'wizard.step5.export_yaml': 'Export YAML',
|
||||
'wizard.step5.load_yaml_file': 'Load YAML file',
|
||||
'wizard.step5.import_yaml': 'Import YAML',
|
||||
'wizard.step5.yaml_document': 'YAML document',
|
||||
'wizard.step5.yaml_placeholder': 'Paste exported YAML here to create or upsert an operation.',
|
||||
'wizard.step5.publish_title': 'Publish',
|
||||
'wizard.step5.publish_subtitle': 'Save the current draft and expose it to published agents.',
|
||||
'wizard.step5.publish_hint': 'Publishing uses the current draft version from this wizard session.',
|
||||
'wizard.step5.publish_action': 'Publish operation',
|
||||
'wizard.step5.editable_title': 'Operation stays editable in this wizard',
|
||||
'wizard.step5.editable_body': 'Saving creates or updates the current draft in place. Use the live actions above to upload samples, test the draft, export or import YAML, and publish when the upstream contract is ready.',
|
||||
'wizard.live.failed_title': 'Live action failed',
|
||||
'wizard.live.failed_body': 'The backend request failed. Review the draft and try again.',
|
||||
'wizard.error.no_workspace': 'No workspace selected',
|
||||
@@ -630,6 +781,10 @@ var TRANSLATIONS = {
|
||||
'nav.workspaces': 'Ваши воркспейсы',
|
||||
'nav.create_workspace': 'Создать воркспейс',
|
||||
'nav.notifications': 'Уведомления',
|
||||
'nav.menu': 'Меню',
|
||||
'nav.account': 'Аккаунт',
|
||||
'nav.user_fallback': 'Crank',
|
||||
'nav.workspace_fallback': 'workspace',
|
||||
|
||||
// Operations page
|
||||
'ops.title': 'Операции',
|
||||
@@ -916,9 +1071,11 @@ var TRANSLATIONS = {
|
||||
'settings.security.capabilities_body': 'Двухфакторная аутентификация, passkeys и инвентарь нескольких сессий запланированы, но в текущем live-flow используется логин по паролю и одна HttpOnly browser session.',
|
||||
'settings.security.not_available': 'Недоступно в этой сборке.',
|
||||
'settings.session.title': 'Текущая сессия',
|
||||
'settings.session.browser': 'Сессия браузера',
|
||||
'settings.session.current': 'текущая',
|
||||
'settings.session.loading': 'Загрузка деталей текущей сессии…',
|
||||
'settings.session.unavailable': 'Детали сессии недоступны. Используйте «Выйти», чтобы отозвать текущую browser session.',
|
||||
'settings.session.current_workspace': 'текущий воркспейс',
|
||||
'settings.session.summary': '{email} · {role} в {workspace}. Используйте «Выйти», чтобы отозвать текущую browser session.',
|
||||
'settings.notifications.title': 'Настройки уведомлений',
|
||||
'settings.notifications.not_ready_title': 'Уведомления пока не подключены.',
|
||||
@@ -988,10 +1145,10 @@ var TRANSLATIONS = {
|
||||
'workspace_setup.danger.delete_body': 'Безвозвратно удаляет все операции, ключи, логи и настройки. Действие нельзя отменить. Вы будете немедленно разлогинены.',
|
||||
'workspace_setup.export': 'Экспорт',
|
||||
'workspace_setup.delete': 'Удалить воркспейс',
|
||||
'workspace_setup.role.owner': 'Owner',
|
||||
'workspace_setup.role.admin': 'Admin',
|
||||
'workspace_setup.role.operator': 'Developer',
|
||||
'workspace_setup.role.viewer': 'Viewer',
|
||||
'workspace_setup.role.owner': 'Владелец',
|
||||
'workspace_setup.role.admin': 'Администратор',
|
||||
'workspace_setup.role.operator': 'Разработчик',
|
||||
'workspace_setup.role.viewer': 'Наблюдатель',
|
||||
'workspace_setup.member_count': '{count} участников',
|
||||
'workspace_setup.member_count_one': '1 участник',
|
||||
'workspace_setup.saving_create': 'Создание…',
|
||||
@@ -1025,6 +1182,7 @@ var TRANSLATIONS = {
|
||||
'wizard.progress.edit': 'Редактировать операцию',
|
||||
'wizard.exit': 'Выйти из мастера',
|
||||
'wizard.step_of': 'Шаг <strong>{step}</strong> из {total}',
|
||||
'wizard.step_short': 'Шаг {step}',
|
||||
'wizard.status.completed': 'Завершено',
|
||||
'wizard.status.in_progress': 'В процессе',
|
||||
'wizard.status.not_started': 'Не начато',
|
||||
@@ -1033,9 +1191,9 @@ var TRANSLATIONS = {
|
||||
'wizard.help.link': 'гайд по сравнению протоколов →',
|
||||
'wizard.progress.section': 'Прогресс',
|
||||
'wizard.step_name.protocol': 'Протокол',
|
||||
'wizard.step_name.upstream': 'Upstream target',
|
||||
'wizard.step_name.upstream': 'Upstream',
|
||||
'wizard.step_name.tool': 'Конфиг инструмента',
|
||||
'wizard.step_name.mapping': 'Mapping',
|
||||
'wizard.step_name.mapping': 'Маппинг',
|
||||
'wizard.button.back': 'Назад',
|
||||
'wizard.button.continue': 'Продолжить',
|
||||
'wizard.button.save_draft': 'Сохранить черновик',
|
||||
@@ -1045,23 +1203,167 @@ var TRANSLATIONS = {
|
||||
'wizard.sidebar.brand.edit': 'Редактировать <span>операцию</span>',
|
||||
'wizard.step1.title': 'Выберите протокол',
|
||||
'wizard.step1.subtitle': 'Выберите транспортный протокол upstream-сервиса. От этого зависит, как Crank будет общаться с вашим API и какие настройки будут доступны на следующих шагах.',
|
||||
'wizard.step1.rest_name': 'REST / HTTP',
|
||||
'wizard.step1.rest_tagline': 'Стандартные HTTP-методы поверх REST endpoint-а. Самый универсальный вариант.',
|
||||
'wizard.step1.graphql_name': 'GraphQL',
|
||||
'wizard.step1.graphql_tagline': 'Запросы и мутации к GraphQL-схеме. Поддерживаются query и mutation для одной фиксированной операции.',
|
||||
'wizard.step1.grpc_name': 'gRPC',
|
||||
'wizard.step1.grpc_tagline': 'Высокопроизводительный бинарный RPC поверх HTTP/2. Подходит для низколатентных внутренних сервисов.',
|
||||
'wizard.step1.query': 'Запрос',
|
||||
'wizard.step1.mutation': 'Мутация',
|
||||
'wizard.step1.unary': 'Unary',
|
||||
'wizard.step1.tip_title': 'Это можно изменить позже',
|
||||
'wizard.step1.tip_body': 'Смена протокола после настройки следующих шагов очистит схемы и mapping. Сохраните операцию как черновик перед экспериментами. См. гайд по миграции протоколов.',
|
||||
'wizard.step2.title': 'Выберите upstream и endpoint',
|
||||
'wizard.step2.subtitle': 'Выберите существующий upstream или зарегистрируйте новый. Затем задайте конкретный путь endpoint-а для этой операции.',
|
||||
'wizard.required': 'Обязательно',
|
||||
'wizard.optional': 'Необязательно',
|
||||
'wizard.step2.upstream_title': 'Upstream',
|
||||
'wizard.step2.upstream_subtitle': 'Общий хост, базовый URL и аутентификация',
|
||||
'wizard.step2.upstream_placeholder': 'Выберите upstream…',
|
||||
'wizard.step2.search_placeholder': 'Поиск по имени или URL…',
|
||||
'wizard.step2.change': 'Изменить',
|
||||
'wizard.step2.register_new': 'Зарегистрировать новый upstream',
|
||||
'wizard.step2.name': 'Имя',
|
||||
'wizard.step2.base_url': 'Base URL',
|
||||
'wizard.step2.unique_hint': 'Уникальный идентификатор этого upstream-а.',
|
||||
'wizard.step2.base_url_hint': 'Корневой URL без завершающего слеша. Поддерживает ${secrets.*}.',
|
||||
'wizard.step2.auth_headers': 'Auth headers',
|
||||
'wizard.step2.auth_headers_hint': 'Эти заголовки отправляются с каждым запросом в этот upstream. Используйте secrets для учетных данных.',
|
||||
'wizard.step2.save_upstream': 'Сохранить upstream',
|
||||
'wizard.step2.endpoint_title': 'Endpoint',
|
||||
'wizard.step2.endpoint_subtitle': 'Путь и HTTP-метод для конкретной операции',
|
||||
'wizard.step2.path_template': 'Шаблон пути',
|
||||
'wizard.step2.path_hint': 'Добавляется к base URL upstream-а. Используйте {param} для path-параметров.',
|
||||
'wizard.step3.rest.label': 'Конфиг запроса',
|
||||
'wizard.step3.graphql.label': 'GQL операция',
|
||||
'wizard.step3.grpc.label': 'RPC метод',
|
||||
'wizard.step3.rest.title': 'HTTP метод и формат',
|
||||
'wizard.step3.rest.subtitle': 'Выберите HTTP-метод, который отправляет эта операция, и настройте content negotiation headers. Crank сериализует аргументы MCP-инструмента в нужный формат request body.',
|
||||
'wizard.step3.rest.method_title': 'HTTP метод',
|
||||
'wizard.step3.rest.method_subtitle': 'Метод, который отправляется в upstream при каждом вызове инструмента',
|
||||
'wizard.step3.rest.read': 'Чтение',
|
||||
'wizard.step3.rest.create': 'Создание',
|
||||
'wizard.step3.rest.replace': 'Замена',
|
||||
'wizard.step3.rest.update': 'Обновление',
|
||||
'wizard.step3.rest.remove': 'Удаление',
|
||||
'wizard.step3.rest.format_title': 'Формат запроса',
|
||||
'wizard.step3.rest.format_subtitle': 'Content negotiation и сериализация',
|
||||
'wizard.step3.rest.content_type': 'Content-Type',
|
||||
'wizard.step3.rest.content_type_hint': 'Как кодируется request body перед отправкой в upstream.',
|
||||
'wizard.step3.rest.accept': 'Accept',
|
||||
'wizard.step3.rest.accept_hint': 'Какие типы ответа принимаются от upstream-сервера.',
|
||||
'wizard.step3.graphql.title': 'GraphQL операция',
|
||||
'wizard.step3.graphql.subtitle': 'Определите GraphQL-операцию, которую будет выполнять этот инструмент. Все запросы отправляются как HTTP POST на endpoint, настроенный на предыдущем шаге. Variables заполняются из аргументов MCP-инструмента во время выполнения.',
|
||||
'wizard.step3.graphql.type_title': 'Тип операции',
|
||||
'wizard.step3.graphql.type_subtitle': 'Вид GraphQL-операции — subscriptions не поддерживаются',
|
||||
'wizard.step3.graphql.query_desc': 'Получение данных без побочных эффектов',
|
||||
'wizard.step3.graphql.mutation_desc': 'Создание, изменение или удаление данных',
|
||||
'wizard.step3.graphql.doc_title': 'Документ запроса',
|
||||
'wizard.step3.graphql.doc_subtitle': 'GraphQL-документ с типизированными объявлениями переменных',
|
||||
'wizard.step3.graphql.variables_title': 'Переменные мапятся на шаге 5',
|
||||
'wizard.step3.graphql.variables_body': 'Объявите все переменные в документе запроса в формате $variable: Type. На шаге 5 вы свяжете поля MCP input-а с этими переменными. Crank автоматически сериализует итоговый payload {"query","variables"}.',
|
||||
'wizard.step3.grpc.title': 'RPC метод',
|
||||
'wizard.step3.grpc.subtitle': 'Выберите, как описывать контракт gRPC-сервиса. Загрузите .proto файл, откройте методы через reflection или введите route вручную. Поддерживаются только unary RPC.',
|
||||
'wizard.step3.grpc.source_proto': 'Proto файл',
|
||||
'wizard.step3.grpc.source_reflection': 'Descriptor set',
|
||||
'wizard.step3.grpc.source_manual': 'Вручную',
|
||||
'wizard.step3.grpc.proto_title': 'Proto definition',
|
||||
'wizard.step3.grpc.proto_subtitle': 'Загрузите или вставьте содержимое .proto файла',
|
||||
'wizard.step3.grpc.drop_title': 'Перетащите сюда .proto файл',
|
||||
'wizard.step3.grpc.drop_subtitle': 'или нажмите для выбора',
|
||||
'wizard.step3.grpc.change_file': 'Сменить файл',
|
||||
'wizard.step3.grpc.paste_content': 'Вставить содержимое',
|
||||
'wizard.step3.grpc.parse_proto': 'Разобрать proto',
|
||||
'wizard.step3.grpc.streaming_excluded': 'Стриминговые методы исключены',
|
||||
'wizard.step3.grpc.streaming_hidden': 'Найдено и скрыто streaming method(s): {count} — они несовместимы с MCP tool calls.',
|
||||
'wizard.step3.grpc.select_method': 'Выберите метод',
|
||||
'wizard.step3.grpc.discovery_live_title': 'Поиск через descriptor set',
|
||||
'wizard.step3.grpc.discovery_live_subtitle': 'Используйте сохраненный черновик и descriptor set, чтобы открыть сервисы через backend',
|
||||
'wizard.step3.grpc.no_upstream': 'Upstream не выбран',
|
||||
'wizard.step3.grpc.select_upstream_first': 'Сначала выберите upstream на шаге 2',
|
||||
'wizard.step3.grpc.open_tools': 'Открыть live-инструменты descriptor-а',
|
||||
'wizard.step3.grpc.connecting': 'Подключение…',
|
||||
'wizard.step3.grpc.live_step5': 'Live-поиск descriptor-а выполняется на шаге 5.',
|
||||
'wizard.step3.grpc.retry': 'Повторить',
|
||||
'wizard.step3.grpc.manual_title': 'Маршрут метода',
|
||||
'wizard.step3.grpc.manual_subtitle': 'Полный gRPC path до unary-метода',
|
||||
'wizard.step3.grpc.route': 'gRPC route',
|
||||
'wizard.step3.grpc.route_hint': 'Формат: /[package.]ServiceName/MethodName',
|
||||
'wizard.step3.grpc.request_type': 'Тип запроса',
|
||||
'wizard.step3.grpc.response_type': 'Тип ответа',
|
||||
'wizard.step3.grpc.proto_doc_hint': 'Имя Protobuf-сообщения, только для документации.',
|
||||
'wizard.step3.grpc.schema_title': 'Входная и выходная схемы задаются на шаге 4',
|
||||
'wizard.step3.grpc.schema_body': 'В ручном режиме нужен только route. Входная и выходная схемы MCP-инструмента — которые LLM использует при вызове — настраиваются на следующем шаге независимо от Protobuf-определений.',
|
||||
'wizard.step3.grpc.signature_title': 'Сигнатура метода',
|
||||
'wizard.step3.grpc.route_label': 'gRPC route',
|
||||
'wizard.step3.grpc.request_label': 'Запрос',
|
||||
'wizard.step3.grpc.response_label': 'Ответ',
|
||||
'wizard.step4.title': 'Конфиг инструмента',
|
||||
'wizard.step4.subtitle': 'Задайте имя инструмента, описание для LLM и определите входную/выходную схему. Описание — главный сигнал, по которому модель решает, вызывать ли этот инструмент.',
|
||||
'wizard.step4.identity_title': 'Идентичность инструмента',
|
||||
'wizard.step4.identity_subtitle': 'Машиночитаемое имя и описание для LLM',
|
||||
'wizard.step4.tool_name': 'Имя инструмента',
|
||||
'wizard.step4.tool_name_hint': 'Машиночитаемый идентификатор для MCP tool calls. Только строчные буквы, цифры и подчеркивания. После публикации изменить нельзя.',
|
||||
'wizard.step4.display_name': 'Отображаемое имя',
|
||||
'wizard.step4.display_name_hint': 'Человеко-читаемое имя для консоли и журнала аудита.',
|
||||
'wizard.step4.tool_title': 'Tool title',
|
||||
'wizard.step4.tool_title_hint': 'Короткая императивная фраза, которая попадет в MCP tool manifest.',
|
||||
'wizard.step4.description': 'Описание',
|
||||
'wizard.step4.description_hint': 'Описание для LLM. Пишите точно — это главный сигнал, по которому модель решает, нужно ли вызывать инструмент.',
|
||||
'wizard.step4.description_title': 'Как писать хорошие описания',
|
||||
'wizard.step4.description_body': 'Начинайте с глагола ("Создает", "Получает", "Обновляет"). Укажите ключевые требования ко входу. Опишите, как выглядит успешный ответ. Избегайте расплывчатых формулировок вроде "обрабатывает" или "управляет". См. рекомендации по описаниям →',
|
||||
'wizard.step4.input_schema_title': 'Входная схема',
|
||||
'wizard.step4.input_schema_subtitle': 'Параметры, которые LLM передает при вызове инструмента',
|
||||
'wizard.step4.output_schema_title': 'Выходная схема',
|
||||
'wizard.step4.output_schema_subtitle': 'Форма данных, которые LLM получает после успешного вызова',
|
||||
'wizard.step4.schema_draft': 'JSON Schema draft-07',
|
||||
'wizard.step4.protocol_agnostic_title': 'Схемы не зависят от протокола',
|
||||
'wizard.step4.protocol_agnostic_body': 'Эти схемы описывают MCP-контракт, а не wire-format upstream-а. Маппинг на шаге 5 переводит одно в другое. Можно загрузить sample JSON-ответа, чтобы автоматически сгенерировать output schema.',
|
||||
'wizard.step5.title': 'Mapping и исполнение',
|
||||
'wizard.step5.subtitle': 'Определите, как аргументы MCP-инструмента мапятся в поля upstream-запроса и как ответ upstream-а мапится обратно в MCP output. Затем задайте execution-параметры — auth настраивается на уровне upstream на шаге 3.',
|
||||
'wizard.step5.input_request': 'Input → Request',
|
||||
'wizard.step5.output_response': 'Response → Output',
|
||||
'wizard.step5.execution': 'Исполнение',
|
||||
'wizard.step5.exec_title': 'Execution config',
|
||||
'wizard.step5.exec_subtitle': 'Таймауты, retry policy и ссылка на auth profile',
|
||||
'wizard.step5.live_title': 'Live-проверка и публикация',
|
||||
'wizard.step5.draft_actions': 'Действия черновика',
|
||||
'wizard.step5.live_save_title': 'Live-действия сначала сохраняют черновик',
|
||||
'wizard.step5.live_save_body': 'Загрузка sample-ов, тестовые прогоны, YAML-действия, публикация и gRPC descriptor discovery сначала сохраняют текущий черновик перед вызовом backend-а.',
|
||||
'wizard.step5.samples_title': 'Samples и генерация черновика',
|
||||
'wizard.step5.samples_subtitle': 'Сохраните входные и выходные sample-ы, затем сгенерируйте схемы и маппинг из реальных payload-ов.',
|
||||
'wizard.step5.input_sample': 'Входной sample',
|
||||
'wizard.step5.output_sample': 'Выходной sample',
|
||||
'wizard.step5.save_input_sample': 'Сохранить входной sample',
|
||||
'wizard.step5.save_output_sample': 'Сохранить выходной sample',
|
||||
'wizard.step5.generate_draft': 'Сгенерировать черновик из sample-ов',
|
||||
'wizard.step5.test_title': 'Тестовый прогон',
|
||||
'wizard.step5.test_subtitle': 'Проверьте текущий черновик на upstream-е перед публикацией.',
|
||||
'wizard.step5.test_input_payload': 'Входной payload для теста',
|
||||
'wizard.step5.run_test': 'Запустить тест',
|
||||
'wizard.step5.use_response_output': 'Использовать ответ как выходной sample',
|
||||
'wizard.step5.request_preview': 'Предпросмотр запроса',
|
||||
'wizard.step5.response_preview': 'Предпросмотр ответа',
|
||||
'wizard.step5.errors': 'Ошибки',
|
||||
'wizard.step5.grpc_descriptor_title': 'gRPC descriptor set',
|
||||
'wizard.step5.grpc_descriptor_subtitle': 'Загрузите скомпилированный descriptor set и откройте unary-методы из сохраненного черновика.',
|
||||
'wizard.step5.choose_descriptor': 'Выбрать descriptor set',
|
||||
'wizard.step5.upload_discover': 'Загрузить и открыть сервисы',
|
||||
'wizard.step5.no_descriptor': 'Descriptor set не выбран',
|
||||
'wizard.step5.yaml_title': 'Импорт и экспорт YAML',
|
||||
'wizard.step5.yaml_subtitle': 'Переносите черновики операций между окружениями, не выходя из мастера.',
|
||||
'wizard.step5.export_yaml': 'Экспорт YAML',
|
||||
'wizard.step5.load_yaml_file': 'Загрузить YAML-файл',
|
||||
'wizard.step5.import_yaml': 'Импорт YAML',
|
||||
'wizard.step5.yaml_document': 'YAML-документ',
|
||||
'wizard.step5.yaml_placeholder': 'Вставьте экспортированный YAML сюда, чтобы создать или обновить операцию.',
|
||||
'wizard.step5.publish_title': 'Публикация',
|
||||
'wizard.step5.publish_subtitle': 'Сохраните текущий черновик и откройте его опубликованным агентам.',
|
||||
'wizard.step5.publish_hint': 'Публикация использует текущую версию черновика из этой сессии мастера.',
|
||||
'wizard.step5.publish_action': 'Опубликовать операцию',
|
||||
'wizard.step5.editable_title': 'Операция остается редактируемой в этом мастере',
|
||||
'wizard.step5.editable_body': 'Сохранение создает или обновляет текущий черновик на месте. Используйте live-действия выше, чтобы загрузить sample-ы, протестировать черновик, экспортировать или импортировать YAML и опубликовать контракт, когда он будет готов.',
|
||||
'wizard.live.failed_title': 'Не удалось выполнить live-действие',
|
||||
'wizard.live.failed_body': 'Запрос к backend завершился ошибкой. Проверьте черновик и попробуйте еще раз.',
|
||||
'wizard.error.no_workspace': 'Воркспейс не выбран',
|
||||
@@ -1261,6 +1563,10 @@ function applyLang() {
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
|
||||
el.title = t(el.getAttribute('data-i18n-title'));
|
||||
});
|
||||
// aria-label
|
||||
document.querySelectorAll('[data-i18n-aria-label]').forEach(function(el) {
|
||||
el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria-label')));
|
||||
});
|
||||
// highlight active language button in settings
|
||||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||||
document.querySelectorAll('.lang-btn').forEach(function(btn) {
|
||||
|
||||
+16
-2
@@ -1,5 +1,19 @@
|
||||
(function () {
|
||||
var STORAGE_KEY = 'crank_user';
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
|
||||
function roleLabel(role) {
|
||||
var normalized = String(role || 'viewer').toLowerCase();
|
||||
if (normalized === 'operator' || normalized === 'developer') return tKey('workspace_setup.role.operator');
|
||||
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
|
||||
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
|
||||
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
|
||||
return normalized.replace(/^\w/, function (character) {
|
||||
return character.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function currentUser() {
|
||||
try {
|
||||
@@ -23,11 +37,11 @@
|
||||
var roleElement = document.querySelector('.user-dropdown-role');
|
||||
|
||||
if (nameElement) {
|
||||
nameElement.textContent = user.name || 'Operator';
|
||||
nameElement.textContent = user.name || tKey('nav.user_fallback');
|
||||
}
|
||||
|
||||
if (roleElement) {
|
||||
roleElement.textContent = (user.role || 'Viewer') + ' · ' + (user.workspace || 'workspace');
|
||||
roleElement.textContent = roleLabel(user.role) + ' · ' + (user.workspace || tKey('nav.workspace_fallback'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,12 @@ function setStatus(id, text, isError) {
|
||||
}
|
||||
|
||||
function titleCaseRole(role) {
|
||||
return String(role || 'viewer').replace(/^\w/, function(character) {
|
||||
var normalized = String(role || 'viewer').toLowerCase();
|
||||
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
|
||||
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
|
||||
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
|
||||
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
|
||||
return normalized.replace(/^\w/, function(character) {
|
||||
return character.toUpperCase();
|
||||
});
|
||||
}
|
||||
@@ -56,7 +61,7 @@ function populateCurrentSession(session) {
|
||||
|
||||
var workspaceLabel = membership && membership.workspace
|
||||
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
|
||||
: 'current workspace';
|
||||
: tKey('settings.session.current_workspace');
|
||||
var roleLabel = membership ? titleCaseRole(membership.role) : tKey('workspace_setup.role.viewer');
|
||||
summary.textContent = tfKey('settings.session.summary', {
|
||||
email: session.user.email,
|
||||
@@ -72,7 +77,7 @@ function populateProfile(session) {
|
||||
|
||||
var user = session.user;
|
||||
document.getElementById('profile-avatar').textContent = initials(user.display_name, user.email);
|
||||
document.getElementById('profile-display-name').textContent = user.display_name || 'Operator';
|
||||
document.getElementById('profile-display-name').textContent = user.display_name || 'Crank';
|
||||
document.getElementById('profile-email').textContent = user.email || '';
|
||||
|
||||
var firstName = document.getElementById('field-firstname');
|
||||
|
||||
+21
-6
@@ -60,6 +60,11 @@ function goToStep(n) {
|
||||
}
|
||||
|
||||
function _doGoToStep(n) {
|
||||
document.querySelectorAll('.step-number[data-step]').forEach(function(element) {
|
||||
var step = Number(element.getAttribute('data-step')) || 0;
|
||||
element.textContent = tfKey('wizard.step_short', { step: step });
|
||||
});
|
||||
|
||||
// 1. hide all panes, show target (step 4 is protocol-specific)
|
||||
document.querySelectorAll('.step-pane').forEach(function(p) { p.style.display = 'none'; });
|
||||
var panelId = (n === 3) ? step3PanelId() : 'step-panel-' + n;
|
||||
@@ -95,7 +100,7 @@ function _doGoToStep(n) {
|
||||
if (pctEl) pctEl.textContent = pct + '%';
|
||||
|
||||
// 4. update step counter
|
||||
var counter = document.querySelector('.step-counter');
|
||||
var counter = document.querySelector('[data-step-counter]');
|
||||
if (counter) counter.innerHTML = tfKey('wizard.step_of', { step: n, total: TOTAL_STEPS });
|
||||
|
||||
// 5. back button
|
||||
@@ -240,9 +245,9 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
var METHOD_CALLOUTS = {
|
||||
GET: null,
|
||||
DELETE: null,
|
||||
POST: { icon: 'info', text: '<strong>POST selected — body encoding required.</strong> In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.' },
|
||||
PUT: { icon: 'info', text: '<strong>PUT selected — full-resource replacement.</strong> The request body must contain a complete resource representation. Define the body schema in step 4.' },
|
||||
PATCH: { icon: 'info', text: '<strong>PATCH selected — partial update.</strong> Only include the fields you want to modify in the body schema defined in step 4.' },
|
||||
POST: { icon: 'info', text: { en: '<strong>POST selected — body encoding required.</strong> In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.', ru: '<strong>Выбран POST — требуется кодирование тела запроса.</strong> На шаге 4 вы зададите JSON-схему для body payload. Crank автоматически сериализует аргументы MCP-инструмента в выбранный формат.' } },
|
||||
PUT: { icon: 'info', text: { en: '<strong>PUT selected — full-resource replacement.</strong> The request body must contain a complete resource representation. Define the body schema in step 4.', ru: '<strong>Выбран PUT — полная замена ресурса.</strong> Request body должен содержать полное представление ресурса. Задайте body schema на шаге 4.' } },
|
||||
PATCH: { icon: 'info', text: { en: '<strong>PATCH selected — partial update.</strong> Only include the fields you want to modify in the body schema defined in step 4.', ru: '<strong>Выбран PATCH — частичное обновление.</strong> В body schema на шаге 4 включайте только те поля, которые хотите изменить.' } },
|
||||
};
|
||||
|
||||
function selectMethod(btn) {
|
||||
@@ -253,9 +258,11 @@ function selectMethod(btn) {
|
||||
if (!callout) return;
|
||||
var info = METHOD_CALLOUTS[method];
|
||||
if (info) {
|
||||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||||
var text = typeof info.text === 'string' ? info.text : (info.text[lang] || info.text.en);
|
||||
callout.innerHTML =
|
||||
'<svg width="15" height="15" style="flex-shrink:0;color:var(--accent)"><use href="' + (window.APP_BASE||'') + 'icons/general/info-circle.svg#icon"/></svg>' +
|
||||
'<span>' + info.text + '</span>';
|
||||
'<span>' + text + '</span>';
|
||||
callout.style.display = 'flex';
|
||||
} else {
|
||||
callout.style.display = 'none';
|
||||
@@ -420,7 +427,7 @@ function startNewUpstream() {
|
||||
|
||||
// Clear combobox trigger & hide preview
|
||||
var val = document.getElementById('upstream-combobox-value');
|
||||
if (val) val.innerHTML = '<span class="upstream-combobox-placeholder">' + tKey('wizard.step2.title') + '</span>';
|
||||
if (val) val.innerHTML = '<span class="upstream-combobox-placeholder">' + tKey('wizard.step2.upstream_placeholder') + '</span>';
|
||||
var preview = document.getElementById('upstream-preview');
|
||||
if (preview) preview.style.display = 'none';
|
||||
selectedUpstreamId = null;
|
||||
@@ -639,6 +646,10 @@ function renderServiceResults(parsed, ids) {
|
||||
}
|
||||
|
||||
function renderParsedProto(parsed) {
|
||||
var notice = document.getElementById('proto-streaming-text');
|
||||
if (notice && parsed && parsed.streamingCount) {
|
||||
notice.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount });
|
||||
}
|
||||
renderServiceResults(parsed, {
|
||||
notice: 'proto-streaming-notice', count: 'proto-streaming-count',
|
||||
summary: 'proto-services-summary', list: 'proto-services-list', view: 'proto-parsed-view',
|
||||
@@ -646,6 +657,10 @@ function renderParsedProto(parsed) {
|
||||
}
|
||||
|
||||
function renderReflectionResult(parsed) {
|
||||
var reflectText = document.querySelector('#reflect-streaming-notice .info-callout-text');
|
||||
if (reflectText && parsed && parsed.streamingCount) {
|
||||
reflectText.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount });
|
||||
}
|
||||
renderServiceResults(parsed, {
|
||||
notice: 'reflect-streaming-notice', count: 'reflect-streaming-count',
|
||||
summary: 'reflect-services-summary', list: 'reflect-services-list', view: 'reflect-parsed-view',
|
||||
|
||||
@@ -125,12 +125,12 @@ function addMemberRow(container, membership) {
|
||||
(
|
||||
canManage
|
||||
? '<select class="member-role-select" data-previous="' + membership.role + '" onchange="updateRole(this, \'' + membership.user.id + '\')">' +
|
||||
'<option value="owner"' + (membership.role === 'owner' ? ' selected' : '') + '>Owner</option>' +
|
||||
'<option value="admin"' + (membership.role === 'admin' ? ' selected' : '') + '>Admin</option>' +
|
||||
'<option value="operator"' + (membership.role === 'operator' ? ' selected' : '') + '>Developer</option>' +
|
||||
'<option value="viewer"' + (membership.role === 'viewer' ? ' selected' : '') + '>Viewer</option>' +
|
||||
'<option value="owner"' + (membership.role === 'owner' ? ' selected' : '') + '>' + tKey('workspace_setup.role.owner') + '</option>' +
|
||||
'<option value="admin"' + (membership.role === 'admin' ? ' selected' : '') + '>' + tKey('workspace_setup.role.admin') + '</option>' +
|
||||
'<option value="operator"' + (membership.role === 'operator' ? ' selected' : '') + '>' + tKey('workspace_setup.role.operator') + '</option>' +
|
||||
'<option value="viewer"' + (membership.role === 'viewer' ? ' selected' : '') + '>' + tKey('workspace_setup.role.viewer') + '</option>' +
|
||||
'</select>' +
|
||||
'<button class="member-remove-btn" type="button" onclick="removeMember(\'' + membership.user.id + '\', \'' + membership.user.display_name.replace(/'/g, "\\'") + '\')" title="Remove member">' +
|
||||
'<button class="member-remove-btn" type="button" onclick="removeMember(\'' + membership.user.id + '\', \'' + membership.user.display_name.replace(/'/g, "\\'") + '\')" title="' + tKey('workspace_setup.members.remove') + '">' +
|
||||
'<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" 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>'
|
||||
: '<span class="member-role-badge ' + roleClass(membership.role) + '">' + roleLabel(membership.role) + '</span>'
|
||||
@@ -189,7 +189,7 @@ function renderInvitations() {
|
||||
'</div>' +
|
||||
'<div class="member-last-active">—</div>' +
|
||||
'<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);">' + roleLabel(invitation.role) + '</span>' +
|
||||
'<button class="member-remove-btn" type="button" onclick="revokeInviteById(\'' + invitation.id + '\')" title="Revoke invite">' +
|
||||
'<button class="member-remove-btn" type="button" onclick="revokeInviteById(\'' + invitation.id + '\')" title="' + tKey('workspace_setup.members.revoke') + '">' +
|
||||
'<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>' +
|
||||
'</button>';
|
||||
pending.appendChild(row);
|
||||
|
||||
Reference in New Issue
Block a user