git list refactor

This commit is contained in:
2026-03-01 12:13:19 +03:00
parent e15eb115c2
commit 4769fbd258
26 changed files with 10313 additions and 2179 deletions

View File

@@ -1,12 +1,12 @@
// [DEF:api_module:Module]
// @TIER: STANDARD
// @SEMANTICS: api, client, fetch, rest
// @PURPOSE: Handles all communication with the backend API.
// @LAYER: Infra-API
import { addToast } from './toasts.js';
import { PUBLIC_WS_URL } from '$env/static/public';
// [DEF:api_module:Module]
// @TIER: STANDARD
// @SEMANTICS: api, client, fetch, rest
// @PURPOSE: Handles all communication with the backend API.
// @LAYER: Infra-API
import { addToast } from './toasts.js';
import { PUBLIC_WS_URL } from '$env/static/public';
const API_BASE_URL = '/api';
// [DEF:buildApiError:Function]
@@ -40,52 +40,69 @@ function notifyApiError(error) {
addToast(error.message, 'error');
}
// [/DEF:notifyApiError:Function]
// [DEF:getWsUrl:Function]
// @PURPOSE: Returns the WebSocket URL for a specific task, with fallback logic.
// @PRE: taskId is provided.
// @POST: Returns valid WebSocket URL string.
// @PARAM: taskId (string) - The ID of the task.
// @RETURN: string - The WebSocket URL.
export const getWsUrl = (taskId) => {
let baseUrl = PUBLIC_WS_URL;
if (!baseUrl) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Use the current host and port to allow Vite proxy to handle the connection
baseUrl = `${protocol}//${window.location.host}`;
}
return `${baseUrl}/ws/logs/${taskId}`;
};
// [/DEF:getWsUrl:Function]
// [DEF:getAuthHeaders:Function]
// @PURPOSE: Returns headers with Authorization if token exists.
function getAuthHeaders() {
const headers = {
'Content-Type': 'application/json',
};
if (typeof window !== 'undefined') {
const token = localStorage.getItem('auth_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
return headers;
}
// [/DEF:getAuthHeaders:Function]
// [DEF:fetchApi:Function]
// @PURPOSE: Generic GET request wrapper.
// @PRE: endpoint string is provided.
// @POST: Returns Promise resolving to JSON data or throws on error.
// @PARAM: endpoint (string) - API endpoint.
// @RETURN: Promise<any> - JSON response.
// [DEF:shouldSuppressApiErrorToast:Function]
// @PURPOSE: Avoid noisy toasts for expected non-critical API failures.
// @PRE: endpoint can be empty; error can be null.
// @POST: Returns true only for explicitly allowed suppressed scenarios.
function shouldSuppressApiErrorToast(endpoint, error) {
const isGitStatusEndpoint =
typeof endpoint === 'string' &&
endpoint.startsWith('/git/repositories/') &&
endpoint.endsWith('/status');
const isNoRepoError =
(error?.status === 400 || error?.status === 404) &&
/Repository for dashboard .* not found/i.test(String(error?.message || ''));
return isGitStatusEndpoint && isNoRepoError;
}
// [/DEF:shouldSuppressApiErrorToast:Function]
// [DEF:getWsUrl:Function]
// @PURPOSE: Returns the WebSocket URL for a specific task, with fallback logic.
// @PRE: taskId is provided.
// @POST: Returns valid WebSocket URL string.
// @PARAM: taskId (string) - The ID of the task.
// @RETURN: string - The WebSocket URL.
export const getWsUrl = (taskId) => {
let baseUrl = PUBLIC_WS_URL;
if (!baseUrl) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Use the current host and port to allow Vite proxy to handle the connection
baseUrl = `${protocol}//${window.location.host}`;
}
return `${baseUrl}/ws/logs/${taskId}`;
};
// [/DEF:getWsUrl:Function]
// [DEF:getAuthHeaders:Function]
// @PURPOSE: Returns headers with Authorization if token exists.
function getAuthHeaders() {
const headers = {
'Content-Type': 'application/json',
};
if (typeof window !== 'undefined') {
const token = localStorage.getItem('auth_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
return headers;
}
// [/DEF:getAuthHeaders:Function]
// [DEF:fetchApi:Function]
// @PURPOSE: Generic GET request wrapper.
// @PRE: endpoint string is provided.
// @POST: Returns Promise resolving to JSON data or throws on error.
// @PARAM: endpoint (string) - API endpoint.
// @RETURN: Promise<any> - JSON response.
async function fetchApi(endpoint) {
try {
console.log(`[api.fetchApi][Action] Fetching from context={{'endpoint': '${endpoint}'}}`);
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: getAuthHeaders()
});
try {
console.log(`[api.fetchApi][Action] Fetching from context={{'endpoint': '${endpoint}'}}`);
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: getAuthHeaders()
});
console.log(`[api.fetchApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
if (!response.ok) {
throw await buildApiError(response);
@@ -129,22 +146,22 @@ async function fetchApiBlob(endpoint, options = {}) {
}
}
// [/DEF:fetchApiBlob:Function]
// [DEF:postApi:Function]
// @PURPOSE: Generic POST request wrapper.
// @PRE: endpoint and body are provided.
// @POST: Returns Promise resolving to JSON data or throws on error.
// @PARAM: endpoint (string) - API endpoint.
// @PARAM: body (object) - Request payload.
// @RETURN: Promise<any> - JSON response.
async function postApi(endpoint, body) {
try {
console.log(`[api.postApi][Action] Posting to context={{'endpoint': '${endpoint}'}}`);
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body),
});
// [DEF:postApi:Function]
// @PURPOSE: Generic POST request wrapper.
// @PRE: endpoint and body are provided.
// @POST: Returns Promise resolving to JSON data or throws on error.
// @PARAM: endpoint (string) - API endpoint.
// @PARAM: body (object) - Request payload.
// @RETURN: Promise<any> - JSON response.
async function postApi(endpoint, body) {
try {
console.log(`[api.postApi][Action] Posting to context={{'endpoint': '${endpoint}'}}`);
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body),
});
console.log(`[api.postApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
if (!response.ok) {
throw await buildApiError(response);
@@ -157,22 +174,22 @@ async function postApi(endpoint, body) {
throw error;
}
}
// [/DEF:postApi:Function]
// [DEF:requestApi:Function]
// @PURPOSE: Generic request wrapper.
// @PRE: endpoint and method are provided.
// @POST: Returns Promise resolving to JSON data or throws on error.
async function requestApi(endpoint, method = 'GET', body = null) {
try {
console.log(`[api.requestApi][Action] ${method} to context={{'endpoint': '${endpoint}'}}`);
const options = {
method,
headers: getAuthHeaders(),
};
if (body) {
options.body = JSON.stringify(body);
}
// [/DEF:postApi:Function]
// [DEF:requestApi:Function]
// @PURPOSE: Generic request wrapper.
// @PRE: endpoint and method are provided.
// @POST: Returns Promise resolving to JSON data or throws on error.
async function requestApi(endpoint, method = 'GET', body = null) {
try {
console.log(`[api.requestApi][Action] ${method} to context={{'endpoint': '${endpoint}'}}`);
const options = {
method,
headers: getAuthHeaders(),
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
console.log(`[api.requestApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
if (!response.ok) {
@@ -187,126 +204,129 @@ async function requestApi(endpoint, method = 'GET', body = null) {
return await response.json();
} catch (error) {
console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error);
notifyApiError(error);
if (!shouldSuppressApiErrorToast(endpoint, error)) {
notifyApiError(error);
}
throw error;
}
}
// [/DEF:requestApi:Function]
// [DEF:api:Data]
// @PURPOSE: API client object with specific methods.
export const api = {
fetchApi,
postApi,
requestApi,
getPlugins: () => fetchApi('/plugins'),
// [/DEF:requestApi:Function]
// [DEF:api:Data]
// @PURPOSE: API client object with specific methods.
export const api = {
fetchApi,
postApi,
requestApi,
getPlugins: () => fetchApi('/plugins'),
getTasks: (options = {}) => {
const params = new URLSearchParams();
if (options.limit != null) params.append('limit', String(options.limit));
if (options.offset != null) params.append('offset', String(options.offset));
if (options.status) params.append('status', options.status);
if (options.task_type) params.append('task_type', options.task_type);
if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));
if (Array.isArray(options.plugin_id)) {
options.plugin_id.forEach((pluginId) => params.append('plugin_id', pluginId));
}
const query = params.toString();
return fetchApi(`/tasks${query ? `?${query}` : ''}`);
const params = new URLSearchParams();
if (options.limit != null) params.append('limit', String(options.limit));
if (options.offset != null) params.append('offset', String(options.offset));
if (options.status) params.append('status', options.status);
if (options.task_type) params.append('task_type', options.task_type);
if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));
if (Array.isArray(options.plugin_id)) {
options.plugin_id.forEach((pluginId) => params.append('plugin_id', pluginId));
}
const query = params.toString();
return fetchApi(`/tasks${query ? `?${query}` : ''}`);
},
getTask: (taskId) => fetchApi(`/tasks/${taskId}`),
getTaskLogs: (taskId, options = {}) => {
const params = new URLSearchParams();
if (options.level) params.append('level', options.level);
if (options.source) params.append('source', options.source);
if (options.search) params.append('search', options.search);
if (options.offset != null) params.append('offset', String(options.offset));
if (options.limit != null) params.append('limit', String(options.limit));
const query = params.toString();
return fetchApi(`/tasks/${taskId}/logs${query ? `?${query}` : ''}`);
const params = new URLSearchParams();
if (options.level) params.append('level', options.level);
if (options.source) params.append('source', options.source);
if (options.search) params.append('search', options.search);
if (options.offset != null) params.append('offset', String(options.offset));
if (options.limit != null) params.append('limit', String(options.limit));
const query = params.toString();
return fetchApi(`/tasks/${taskId}/logs${query ? `?${query}` : ''}`);
},
createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),
// Settings
getSettings: () => fetchApi('/settings'),
updateGlobalSettings: (settings) => requestApi('/settings/global', 'PATCH', settings),
getEnvironments: () => fetchApi('/settings/environments'),
addEnvironment: (env) => postApi('/settings/environments', env),
updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),
deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),
testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),
updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule),
getStorageSettings: () => fetchApi('/settings/storage'),
updateStorageSettings: (storage) => requestApi('/settings/storage', 'PUT', storage),
getSettings: () => fetchApi('/settings'),
updateGlobalSettings: (settings) => requestApi('/settings/global', 'PATCH', settings),
getEnvironments: () => fetchApi('/settings/environments'),
addEnvironment: (env) => postApi('/settings/environments', env),
updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),
deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),
testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),
updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule),
getStorageSettings: () => fetchApi('/settings/storage'),
updateStorageSettings: (storage) => requestApi('/settings/storage', 'PUT', storage),
getEnvironmentsList: () => fetchApi('/environments'),
getLlmStatus: () => fetchApi('/llm/status'),
getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),
getStorageFileBlob: (path) =>
fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),
fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),
// Dashboards
getDashboards: (envId, options = {}) => {
const params = new URLSearchParams({ env_id: envId });
if (options.search) params.append('search', options.search);
if (options.page) params.append('page', options.page);
if (options.page_size) params.append('page_size', options.page_size);
return fetchApi(`/dashboards?${params.toString()}`);
const params = new URLSearchParams({ env_id: envId });
if (options.search) params.append('search', options.search);
if (options.page) params.append('page', options.page);
if (options.page_size) params.append('page_size', options.page_size);
return fetchApi(`/dashboards?${params.toString()}`);
},
getDashboardDetail: (envId, dashboardId) => fetchApi(`/dashboards/${dashboardId}?env_id=${envId}`),
getDashboardTaskHistory: (envId, dashboardId, options = {}) => {
const params = new URLSearchParams();
if (envId) params.append('env_id', envId);
if (options.limit) params.append('limit', options.limit);
return fetchApi(`/dashboards/${dashboardId}/tasks?${params.toString()}`);
const params = new URLSearchParams();
if (envId) params.append('env_id', envId);
if (options.limit) params.append('limit', options.limit);
return fetchApi(`/dashboards/${dashboardId}/tasks?${params.toString()}`);
},
getDashboardThumbnail: (envId, dashboardId, options = {}) => {
const params = new URLSearchParams();
params.append('env_id', envId);
if (options.force != null) params.append('force', String(Boolean(options.force)));
return fetchApiBlob(`/dashboards/${dashboardId}/thumbnail?${params.toString()}`, { notifyError: false });
const params = new URLSearchParams();
params.append('env_id', envId);
if (options.force != null) params.append('force', String(Boolean(options.force)));
return fetchApiBlob(`/dashboards/${dashboardId}/thumbnail?${params.toString()}`, { notifyError: false });
},
getDatabaseMappings: (sourceEnvId, targetEnvId) => fetchApi(`/dashboards/db-mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
// Datasets
getDatasets: (envId, options = {}) => {
const params = new URLSearchParams({ env_id: envId });
if (options.search) params.append('search', options.search);
if (options.page) params.append('page', options.page);
if (options.page_size) params.append('page_size', options.page_size);
return fetchApi(`/datasets?${params.toString()}`);
},
getDatasetIds: (envId, options = {}) => {
const params = new URLSearchParams({ env_id: envId });
if (options.search) params.append('search', options.search);
return fetchApi(`/datasets/ids?${params.toString()}`);
},
getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),
// Settings
getConsolidatedSettings: () => fetchApi('/settings/consolidated'),
updateConsolidatedSettings: (settings) => requestApi('/settings/consolidated', 'PATCH', settings),
};
// [/DEF:api:Data]
// [/DEF:api_module:Module]
// Export individual functions for easier use in components
export { requestApi };
export const getPlugins = api.getPlugins;
export const getTasks = api.getTasks;
export const getTask = api.getTask;
export const createTask = api.createTask;
export const getSettings = api.getSettings;
export const updateGlobalSettings = api.updateGlobalSettings;
export const getEnvironments = api.getEnvironments;
export const addEnvironment = api.addEnvironment;
export const updateEnvironment = api.updateEnvironment;
export const deleteEnvironment = api.deleteEnvironment;
export const testEnvironmentConnection = api.testEnvironmentConnection;
export const updateEnvironmentSchedule = api.updateEnvironmentSchedule;
export const getEnvironmentsList = api.getEnvironmentsList;
export const getStorageSettings = api.getStorageSettings;
export const updateStorageSettings = api.updateStorageSettings;
export const getDashboards = api.getDashboards;
export const getDatasets = api.getDatasets;
export const getConsolidatedSettings = api.getConsolidatedSettings;
export const updateConsolidatedSettings = api.updateConsolidatedSettings;
calculateMigrationDryRun: (payload) => postApi('/migration/dry-run', payload),
// Datasets
getDatasets: (envId, options = {}) => {
const params = new URLSearchParams({ env_id: envId });
if (options.search) params.append('search', options.search);
if (options.page) params.append('page', options.page);
if (options.page_size) params.append('page_size', options.page_size);
return fetchApi(`/datasets?${params.toString()}`);
},
getDatasetIds: (envId, options = {}) => {
const params = new URLSearchParams({ env_id: envId });
if (options.search) params.append('search', options.search);
return fetchApi(`/datasets/ids?${params.toString()}`);
},
getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),
// Settings
getConsolidatedSettings: () => fetchApi('/settings/consolidated'),
updateConsolidatedSettings: (settings) => requestApi('/settings/consolidated', 'PATCH', settings),
};
// [/DEF:api:Data]
// [/DEF:api_module:Module]
// Export individual functions for easier use in components
export { requestApi };
export const getPlugins = api.getPlugins;
export const getTasks = api.getTasks;
export const getTask = api.getTask;
export const createTask = api.createTask;
export const getSettings = api.getSettings;
export const updateGlobalSettings = api.updateGlobalSettings;
export const getEnvironments = api.getEnvironments;
export const addEnvironment = api.addEnvironment;
export const updateEnvironment = api.updateEnvironment;
export const deleteEnvironment = api.deleteEnvironment;
export const testEnvironmentConnection = api.testEnvironmentConnection;
export const updateEnvironmentSchedule = api.updateEnvironmentSchedule;
export const getEnvironmentsList = api.getEnvironmentsList;
export const getStorageSettings = api.getStorageSettings;
export const updateStorageSettings = api.updateStorageSettings;
export const getDashboards = api.getDashboards;
export const getDatasets = api.getDatasets;
export const getConsolidatedSettings = api.getConsolidatedSettings;
export const updateConsolidatedSettings = api.updateConsolidatedSettings;

View File

@@ -75,4 +75,14 @@ export function getAssistantConversations(
return requestApi(`/assistant/conversations?${params.toString()}`, 'GET');
}
// [/DEF:getAssistantConversations:Function]
// [DEF:deleteAssistantConversation:Function]
// @PURPOSE: Soft-delete or hard-delete a conversation.
// @PRE: conversationId string is provided.
// @POST: Returns success status.
export function deleteAssistantConversation(conversationId) {
return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE');
}
// [/DEF:deleteAssistantConversation:Function]
// [/DEF:frontend.src.lib.api.assistant:Module]

View File

@@ -56,6 +56,7 @@
cancelAssistantOperation,
getAssistantHistory,
getAssistantConversations,
deleteAssistantConversation,
} from "$lib/api/assistant.js";
import { api } from "$lib/api.js";
import { gitService } from "../../../services/gitService.js";
@@ -175,6 +176,32 @@
}
// [/DEF:loadConversations:Function]
// [DEF:removeConversation:Function]
// @PURPOSE: Removes a conversation from the list and deletes it from the backend.
// @PRE: conversationId string is provided.
// @POST: It is soft-deleted from the API and removed from local UI. If active, reset state.
async function removeConversation(e, conversationIdTemp) {
if (e) {
e.stopPropagation();
e.preventDefault();
}
try {
await deleteAssistantConversation(conversationIdTemp);
conversations = conversations.filter(
(c) => c.conversation_id !== conversationIdTemp,
);
if (conversationId === conversationIdTemp) {
$assistantChatStore.conversationId = null;
$assistantChatStore.messages = [];
$assistantChatStore.state = "idle";
}
addToast("Conversation deleted", "success");
} catch (err) {
addToast("Failed to delete conversation: " + err.message, "error");
}
}
// [/DEF:removeConversation:Function]
// [DEF:loadOlderMessages:Function]
/**
* @PURPOSE: Lazy-load older messages for active conversation when user scrolls to top.
@@ -622,21 +649,30 @@
</div>
<div class="flex gap-2 overflow-x-auto pb-1">
{#each conversations as convo (convo.conversation_id)}
<button
class="min-w-[140px] max-w-[220px] rounded-lg border px-2.5 py-1.5 text-left text-xs transition {convo.conversation_id ===
conversationId
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}"
on:click={() => selectConversation(convo)}
title={formatConversationTime(convo.updated_at)}
>
<div class="truncate font-semibold">
{buildConversationTitle(convo)}
</div>
<div class="truncate text-[10px] text-slate-500">
{convo.last_message || ""}
</div>
</button>
<div class="relative group min-w-[140px] max-w-[220px]">
<button
class="w-full rounded-lg border px-2.5 py-1.5 text-left text-xs transition {convo.conversation_id ===
conversationId
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}"
on:click={() => selectConversation(convo)}
title={formatConversationTime(convo.updated_at)}
>
<div class="truncate font-semibold pr-4">
{buildConversationTitle(convo)}
</div>
<div class="truncate text-[10px] text-slate-500 pr-4">
{convo.last_message || ""}
</div>
</button>
<button
class="absolute right-1.5 top-1.5 hidden group-hover:block p-1 text-slate-400 hover:text-red-500 rounded bg-white/80 hover:bg-red-50"
on:click={(e) => removeConversation(e, convo.conversation_id)}
title="Удалить диалог"
>
<Icon name="trash" size={12} />
</button>
</div>
{/each}
{#if loadingConversations}
<div

View File

@@ -238,6 +238,19 @@
"server": "Git Server",
"not_linked": "This dashboard is not yet linked to a Git repository.",
"manage": "Manage Git",
"manage_selected": "Manage selected",
"select_single_for_manage": "Select exactly one dashboard to manage",
"selected_count": "{count} selected",
"bulk_sync": "Bulk Sync",
"bulk_commit": "Bulk Commit",
"bulk_pull": "Bulk Pull",
"bulk_push": "Bulk Push",
"bulk_action_sync": "Sync",
"bulk_action_commit": "Commit",
"bulk_action_pull": "Pull",
"bulk_action_push": "Push",
"bulk_result": "{action}: {success} success, {failed} failed",
"no_repositories_selected": "No repositories available for selected dashboards",
"generate_message": "Generate",
"select_dashboard": "Select Dashboard to Manage"
,
@@ -269,7 +282,17 @@
"load_branches_failed": "Failed to load branches",
"switched_to": "Switched to {branch}",
"created_branch": "Created branch {branch}",
"branch_name_placeholder": "branch-name"
"branch_name_placeholder": "branch-name",
"repo_status": {
"loading": "Loading",
"no_repo": "No Repo",
"synced": "Synced",
"changes": "Changes",
"behind_remote": "Behind Remote",
"ahead_remote": "Ahead Remote",
"diverged": "Diverged",
"error": "Error"
}
},
"dashboard": {
"search": "Search dashboards...",

View File

@@ -238,9 +238,21 @@
"server": "Git-сервер",
"not_linked": "Этот дашборд еще не привязан к Git-репозиторию.",
"manage": "Управление Git",
"manage_selected": "Управлять выбранным",
"select_single_for_manage": "Выберите ровно один дашборд для управления",
"selected_count": "{count} выбрано",
"bulk_sync": "Массовый sync",
"bulk_commit": "Массовый commit",
"bulk_pull": "Массовый pull",
"bulk_push": "Массовый push",
"bulk_action_sync": "Sync",
"bulk_action_commit": "Commit",
"bulk_action_pull": "Pull",
"bulk_action_push": "Push",
"bulk_result": "{action}: успешно {success}, ошибок {failed}",
"no_repositories_selected": "Для выбранных дашбордов нет доступных репозиториев",
"generate_message": "Сгенерировать",
"select_dashboard": "Выберите дашборд для управления"
,
"select_dashboard": "Выберите дашборд для управления",
"commit_message_generated": "Сообщение коммита сгенерировано",
"commit_message_failed": "Не удалось сгенерировать сообщение коммита",
"load_changes_failed": "Не удалось загрузить изменения",
@@ -269,7 +281,17 @@
"load_branches_failed": "Не удалось загрузить ветки",
"switched_to": "Переключено на {branch}",
"created_branch": "Создана ветка {branch}",
"branch_name_placeholder": "имя-ветки"
"branch_name_placeholder": "имя-ветки",
"repo_status": {
"loading": "Загрузка",
"no_repo": "Нет репозитория",
"synced": "Синхронизирован",
"changes": "Есть изменения",
"behind_remote": "Отстает от remote",
"ahead_remote": "Опережает remote",
"diverged": "Расхождение",
"error": "Ошибка"
}
},
"dashboard": {
"search": "Поиск дашбордов...",
@@ -545,7 +567,9 @@
"mapping_management": "Управление маппингом БД",
"fetch_dbs": "Получить БД и подсказки",
"mapping_hint": "Выберите окружения и нажмите «Получить БД и подсказки», чтобы начать маппинг.",
"task_placeholder_warn": "Не удалось сразу получить детали задачи, используется временное состояние."
"task_placeholder_warn": "Не удалось сразу получить детали задачи, используется временное состояние.",
"calculating_dry_run": "Расчет ожидаемых изменений...",
"dry_run_summary": "Отчет dry-run: всего будет изменено {total} элементов (дашбордов: {dashboards}, чартов: {charts}, датасетов: {datasets})"
},
"assistant": {
"title": "AI Ассистент",

View File

@@ -14,13 +14,12 @@
"M4 7v10",
"M20 7v10",
],
storage: [
"M3 8l9-4 9 4-9 4-9-4z",
"M3 13l9 4 9-4",
"M3 17l9 4 9-4",
],
storage: ["M3 8l9-4 9 4-9 4-9-4z", "M3 13l9 4 9-4", "M3 17l9 4 9-4"],
reports: ["M5 5h14v14H5z", "M8 9h8", "M8 13h8", "M8 17h5"],
admin: ["M12 3l8 4v5c0 5.2-3.4 8.6-8 9.9C7.4 20.6 4 17.2 4 12V7l8-4z", "M9 12l2 2 4-4"],
admin: [
"M12 3l8 4v5c0 5.2-3.4 8.6-8 9.9C7.4 20.6 4 17.2 4 12V7l8-4z",
"M9 12l2 2 4-4",
],
chevronDown: ["M6 9l6 6 6-6"],
chevronLeft: ["M15 6l-6 6 6 6"],
chevronRight: ["M9 6l6 6-6 6"],
@@ -39,9 +38,30 @@
layers: ["M12 4l8 4-8 4-8-4 8-4z", "M4 12l8 4 8-4", "M4 16l8 4 8-4"],
back: ["M19 12H5", "M12 5l-7 7 7 7"],
close: ["M18 6L6 18", "M6 6l12 12"],
list: ["M8 7h12", "M8 12h12", "M8 17h12", "M4 7h.01", "M4 12h.01", "M4 17h.01"],
clipboard: ["M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2", "M9 5a2 2 0 002 2h2a2 2 0 002-2", "M9 5a2 2 0 012-2h2a2 2 0 012 2"],
settings: ["M12 8.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z", "M19.4 15a1 1 0 00.2 1.1l.1.1a1 1 0 010 1.4l-1.1 1.1a1 1 0 01-1.4 0l-.1-.1a1 1 0 00-1.1-.2 1 1 0 00-.6.9V20a1 1 0 01-1 1h-1.6a1 1 0 01-1-1v-.2a1 1 0 00-.6-.9 1 1 0 00-1.1.2l-.1.1a1 1 0 01-1.4 0l-1.1-1.1a1 1 0 010-1.4l.1-.1a1 1 0 00.2-1.1 1 1 0 00-.9-.6H4a1 1 0 01-1-1v-1.6a1 1 0 011-1h.2a1 1 0 00.9-.6 1 1 0 00-.2-1.1l-.1-.1a1 1 0 010-1.4l1.1-1.1a1 1 0 011.4 0l.1.1a1 1 0 001.1.2 1 1 0 00.6-.9V4a1 1 0 011-1h1.6a1 1 0 011 1v.2a1 1 0 00.6.9 1 1 0 001.1-.2l.1-.1a1 1 0 011.4 0l1.1 1.1a1 1 0 010 1.4l-.1.1a1 1 0 00-.2 1.1 1 1 0 00.9.6H20a1 1 0 011 1v1.6a1 1 0 01-1 1h-.2a1 1 0 00-.9.6z"],
list: [
"M8 7h12",
"M8 12h12",
"M8 17h12",
"M4 7h.01",
"M4 12h.01",
"M4 17h.01",
],
clipboard: [
"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2",
"M9 5a2 2 0 002 2h2a2 2 0 002-2",
"M9 5a2 2 0 012-2h2a2 2 0 012 2",
],
settings: [
"M12 8.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z",
"M19.4 15a1 1 0 00.2 1.1l.1.1a1 1 0 010 1.4l-1.1 1.1a1 1 0 01-1.4 0l-.1-.1a1 1 0 00-1.1-.2 1 1 0 00-.6.9V20a1 1 0 01-1 1h-1.6a1 1 0 01-1-1v-.2a1 1 0 00-.6-.9 1 1 0 00-1.1.2l-.1.1a1 1 0 01-1.4 0l-1.1-1.1a1 1 0 010-1.4l.1-.1a1 1 0 00.2-1.1 1 1 0 00-.9-.6H4a1 1 0 01-1-1v-1.6a1 1 0 011-1h.2a1 1 0 00.9-.6 1 1 0 00-.2-1.1l-.1-.1a1 1 0 010-1.4l1.1-1.1a1 1 0 011.4 0l.1.1a1 1 0 001.1.2 1 1 0 00.6-.9V4a1 1 0 011-1h1.6a1 1 0 011 1v.2a1 1 0 00.6.9 1 1 0 001.1-.2l.1-.1a1 1 0 011.4 0l1.1 1.1a1 1 0 010 1.4l-.1.1a1 1 0 00-.2 1.1 1 1 0 00.9.6H20a1 1 0 011 1v1.6a1 1 0 01-1 1h-.2a1 1 0 00-.9.6z",
],
trash: [
"M3 6h18",
"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",
"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",
"M10 11v6",
"M14 11v6",
],
};
$: paths = iconPaths[name] || iconPaths.dashboard;
@@ -61,6 +81,6 @@
aria-hidden="true"
>
{#each paths as d}
<path d={d} />
<path {d} />
{/each}
</svg>