From 1042b35d1bc67a627dba30a6030ecb7b7e096705 Mon Sep 17 00:00:00 2001 From: busya Date: Mon, 26 Jan 2026 22:12:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=BA=D0=BE=D0=BD=D1=87=D0=B8?= =?UTF-8?q?=D0=BB=D0=B8=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD?= =?UTF-8?q?,=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D0=B1=D1=8D?= =?UTF-8?q?=D0=BA=D0=B0=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/api/routes/environments.py | 2 +- backend/src/core/plugin_base.py | 18 +- backend/src/core/plugin_loader.py | 1 + backend/src/plugins/backup.py | 9 + backend/src/plugins/debug.py | 9 + backend/src/plugins/git_plugin.py | 9 + backend/src/plugins/mapper.py | 9 + backend/src/plugins/migration.py | 9 + backend/src/plugins/search.py | 9 + backend/src/plugins/storage/plugin.py | 9 + backend/tasks.db | Bin 98304 -> 98304 bytes frontend/src/components/EnvSelector.svelte | 60 +++++ .../src/components/backups/BackupList.svelte | 14 +- .../components/backups/BackupManager.svelte | 241 ++++++++++++++++++ .../src/components/tools/MapperTool.svelte | 160 ++++++------ frontend/src/lib/api.js | 6 +- frontend/src/routes/+page.svelte | 12 +- .../src/routes/tools/storage/+page.svelte | 16 +- .../contracts/backup_contracts.md | 13 +- specs/015-frontend-nav-redesign/data-model.md | 47 ++++ specs/015-frontend-nav-redesign/plan.md | 7 + specs/015-frontend-nav-redesign/spec.md | 2 + specs/015-frontend-nav-redesign/tasks.md | 4 + 23 files changed, 572 insertions(+), 94 deletions(-) create mode 100644 frontend/src/components/EnvSelector.svelte create mode 100644 specs/015-frontend-nav-redesign/data-model.md diff --git a/backend/src/api/routes/environments.py b/backend/src/api/routes/environments.py index a74d128..cff6847 100644 --- a/backend/src/api/routes/environments.py +++ b/backend/src/api/routes/environments.py @@ -23,7 +23,7 @@ router = APIRouter() # [DEF:ScheduleSchema:DataClass] class ScheduleSchema(BaseModel): enabled: bool = False - cron_expression: str = Field(..., pattern=r'^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|((((\d+,)*\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$') + cron_expression: str = Field(..., pattern=r'^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|((((\d+,)*\d+|(\d+(\/|-)\d+)|\d+|\*) ?){4,6})$') # [/DEF:ScheduleSchema:DataClass] # [DEF:EnvironmentResponse:DataClass] diff --git a/backend/src/core/plugin_base.py b/backend/src/core/plugin_base.py index 64a4d85..82e19fc 100755 --- a/backend/src/core/plugin_base.py +++ b/backend/src/core/plugin_base.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, Any +from typing import Dict, Any, Optional from .logger import belief_scope from pydantic import BaseModel, Field @@ -68,6 +68,21 @@ class PluginBase(ABC): pass # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the plugin's UI, if applicable. + # @PRE: Plugin instance exists. + # @POST: Returns string route or None. + # @RETURN: Optional[str] - Frontend route. + def ui_route(self) -> Optional[str]: + """ + The frontend route for the plugin's UI. + Returns None if the plugin does not have a dedicated UI page. + """ + with belief_scope("ui_route"): + return None + # [/DEF:ui_route:Function] + @abstractmethod # [DEF:get_schema:Function] # @PURPOSE: Returns the JSON schema for the plugin's input parameters. @@ -111,5 +126,6 @@ class PluginConfig(BaseModel): name: str = Field(..., description="Human-readable name for the plugin") description: str = Field(..., description="Brief description of what the plugin does") version: str = Field(..., description="Version of the plugin") + ui_route: Optional[str] = Field(None, description="Frontend route for the plugin UI") input_schema: Dict[str, Any] = Field(..., description="JSON schema for input parameters", alias="schema") # [/DEF:PluginConfig:Class] \ No newline at end of file diff --git a/backend/src/core/plugin_loader.py b/backend/src/core/plugin_loader.py index afcc21e..c786ad7 100755 --- a/backend/src/core/plugin_loader.py +++ b/backend/src/core/plugin_loader.py @@ -141,6 +141,7 @@ class PluginLoader: name=plugin_instance.name, description=plugin_instance.description, version=plugin_instance.version, + ui_route=plugin_instance.ui_route, schema=schema, ) # The following line is commented out because it requires a schema to be passed to validate against. diff --git a/backend/src/plugins/backup.py b/backend/src/plugins/backup.py index 1d68d8a..e6d221e 100755 --- a/backend/src/plugins/backup.py +++ b/backend/src/plugins/backup.py @@ -75,6 +75,15 @@ class BackupPlugin(PluginBase): return "1.0.0" # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the backup plugin. + # @RETURN: str - "/tools/backups" + def ui_route(self) -> str: + with belief_scope("ui_route"): + return "/tools/backups" + # [/DEF:ui_route:Function] + # [DEF:get_schema:Function] # @PURPOSE: Returns the JSON schema for backup plugin parameters. # @PRE: Plugin instance exists. diff --git a/backend/src/plugins/debug.py b/backend/src/plugins/debug.py index 5394f2f..bbe8ba4 100644 --- a/backend/src/plugins/debug.py +++ b/backend/src/plugins/debug.py @@ -63,6 +63,15 @@ class DebugPlugin(PluginBase): return "1.0.0" # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the debug plugin. + # @RETURN: str - "/tools/debug" + def ui_route(self) -> str: + with belief_scope("ui_route"): + return "/tools/debug" + # [/DEF:ui_route:Function] + # [DEF:get_schema:Function] # @PURPOSE: Returns the JSON schema for the debug plugin parameters. # @PRE: Plugin instance exists. diff --git a/backend/src/plugins/git_plugin.py b/backend/src/plugins/git_plugin.py index 0eeb7ff..8fb3e92 100644 --- a/backend/src/plugins/git_plugin.py +++ b/backend/src/plugins/git_plugin.py @@ -99,6 +99,15 @@ class GitPlugin(PluginBase): return "0.1.0" # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the git plugin. + # @RETURN: str - "/git" + def ui_route(self) -> str: + with belief_scope("GitPlugin.ui_route"): + return "/git" + # [/DEF:ui_route:Function] + # [DEF:get_schema:Function] # @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина. # @PRE: GitPlugin is initialized. diff --git a/backend/src/plugins/mapper.py b/backend/src/plugins/mapper.py index c682004..92f136c 100644 --- a/backend/src/plugins/mapper.py +++ b/backend/src/plugins/mapper.py @@ -66,6 +66,15 @@ class MapperPlugin(PluginBase): return "1.0.0" # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the mapper plugin. + # @RETURN: str - "/tools/mapper" + def ui_route(self) -> str: + with belief_scope("ui_route"): + return "/tools/mapper" + # [/DEF:ui_route:Function] + # [DEF:get_schema:Function] # @PURPOSE: Returns the JSON schema for the mapper plugin parameters. # @PRE: Plugin instance exists. diff --git a/backend/src/plugins/migration.py b/backend/src/plugins/migration.py index 40e6dc0..6270d8e 100755 --- a/backend/src/plugins/migration.py +++ b/backend/src/plugins/migration.py @@ -71,6 +71,15 @@ class MigrationPlugin(PluginBase): return "1.0.0" # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the migration plugin. + # @RETURN: str - "/migration" + def ui_route(self) -> str: + with belief_scope("ui_route"): + return "/migration" + # [/DEF:ui_route:Function] + # [DEF:get_schema:Function] # @PURPOSE: Returns the JSON schema for migration plugin parameters. # @PRE: Config manager is available. diff --git a/backend/src/plugins/search.py b/backend/src/plugins/search.py index 9a39949..b29b7fe 100644 --- a/backend/src/plugins/search.py +++ b/backend/src/plugins/search.py @@ -64,6 +64,15 @@ class SearchPlugin(PluginBase): return "1.0.0" # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the search plugin. + # @RETURN: str - "/tools/search" + def ui_route(self) -> str: + with belief_scope("ui_route"): + return "/tools/search" + # [/DEF:ui_route:Function] + # [DEF:get_schema:Function] # @PURPOSE: Returns the JSON schema for the search plugin parameters. # @PRE: Plugin instance exists. diff --git a/backend/src/plugins/storage/plugin.py b/backend/src/plugins/storage/plugin.py index 8fd20b4..459d381 100644 --- a/backend/src/plugins/storage/plugin.py +++ b/backend/src/plugins/storage/plugin.py @@ -82,6 +82,15 @@ class StoragePlugin(PluginBase): return "1.0.0" # [/DEF:version:Function] + @property + # [DEF:ui_route:Function] + # @PURPOSE: Returns the frontend route for the storage plugin. + # @RETURN: str - "/tools/storage" + def ui_route(self) -> str: + with belief_scope("StoragePlugin:ui_route"): + return "/tools/storage" + # [/DEF:ui_route:Function] + # [DEF:get_schema:Function] # @PURPOSE: Returns the JSON schema for storage plugin parameters. # @PRE: None. diff --git a/backend/tasks.db b/backend/tasks.db index 10e028dd36e821d9cd65a33eb42727b73fe15cd3..bfa202c74de5b1c749fda2316799bea8fc68a47a 100644 GIT binary patch delta 323 zcmZo@U~6b#n;^|Qg@J)V0*Jw2qJ}x+l#K~X@&#D<-+)BK`G50&P&d`sEK%3Qz#>)G zBE`r^*U~uIG{w>+)hszBMPhSxPwQnaUZC-ed~ytYazL|1_)MDPC3r)%rG*_GTk;u< z?L~<(YI;Eoqr~L69h^EwW(tOuRt82^1_pWth6ZL921W)(X1WH3x(FE)Jqr_46JyiK z8@EXa8JUG3X)rZ5Gn(vpO?A@tC?pw>1|u_LOQ1|W8^nU?_v0B=x3d&5{^17zRO(Z# delta 101 zcmZo@U~6b#n;^~G&%nSS0mR}!%s5fQoUwmn!jgPGX8w;r0R}Pt-~8YBKW-Kjc*H;X zOT7S}C{Hy5&o}<-e4@OCJZCqHG}!S>T%f(Vx~KK>v;sz!&7uOY`6n(A-Of_L_=g_= DiT@v- diff --git a/frontend/src/components/EnvSelector.svelte b/frontend/src/components/EnvSelector.svelte new file mode 100644 index 0000000..78eda60 --- /dev/null +++ b/frontend/src/components/EnvSelector.svelte @@ -0,0 +1,60 @@ + + + + + + +
+ + +
+ + + + + \ No newline at end of file diff --git a/frontend/src/components/backups/BackupList.svelte b/frontend/src/components/backups/BackupList.svelte index 333a6bc..72d4392 100644 --- a/frontend/src/components/backups/BackupList.svelte +++ b/frontend/src/components/backups/BackupList.svelte @@ -11,9 +11,14 @@ import { t } from '../../lib/i18n'; import { Button } from '../../lib/ui'; import type { Backup } from '../../types/backup'; + import { goto } from '$app/navigation'; // [/SECTION] // [SECTION: PROPS] + /** + * @type {Backup[]} + * @description Array of backup objects to display. + */ export let backups: Backup[] = []; // [/SECTION] @@ -51,8 +56,13 @@ {new Date(backup.created_at).toLocaleString()} - diff --git a/frontend/src/components/backups/BackupManager.svelte b/frontend/src/components/backups/BackupManager.svelte index e69de29..e5149aa 100644 --- a/frontend/src/components/backups/BackupManager.svelte +++ b/frontend/src/components/backups/BackupManager.svelte @@ -0,0 +1,241 @@ + + + + + + +
+ +
+
+
+ +
+ {$t.tasks.schedule_enabled} +
+ +
+ +
+ +

{$t.tasks.cron_hint}

+
+ +
+ +
+
+
+ + {/if} + + + +
+

{$t.storage.backups}

+ {#if loading} +
{$t.common.loading}
+ {:else} + + {/if} +
+ + + + \ No newline at end of file diff --git a/frontend/src/components/tools/MapperTool.svelte b/frontend/src/components/tools/MapperTool.svelte index 6fbc242..93b0cf7 100644 --- a/frontend/src/components/tools/MapperTool.svelte +++ b/frontend/src/components/tools/MapperTool.svelte @@ -13,6 +13,8 @@ import { getConnections } from '../../services/connectionService.js'; import { selectedTask } from '../../lib/stores.js'; import { addToast } from '../../lib/toasts.js'; + import { t } from '../../lib/i18n'; + import { Button, Card, Select, Input } from '../../lib/ui'; // [/SECTION] let envs = []; @@ -36,7 +38,7 @@ envs = await envsRes.json(); connections = await getConnections(); } catch (e) { - addToast('Failed to fetch data', 'error'); + addToast($t.mapper.errors.fetch_failed, 'error'); } } // [/DEF:fetchData:Function] @@ -47,17 +49,17 @@ // @POST: Mapper task is started and selectedTask is updated. async function handleRunMapper() { if (!selectedEnv || !datasetId) { - addToast('Please fill in required fields', 'warning'); + addToast($t.mapper.errors.required_fields, 'warning'); return; } if (source === 'postgres' && (!selectedConnection || !tableName)) { - addToast('Connection and Table Name are required for postgres source', 'warning'); + addToast($t.mapper.errors.postgres_required, 'warning'); return; } if (source === 'excel' && !excelPath) { - addToast('Excel path is required for excel source', 'warning'); + addToast($t.mapper.errors.excel_required, 'warning'); return; } @@ -75,7 +77,7 @@ }); selectedTask.set(task); - addToast('Mapper task started', 'success'); + addToast($t.mapper.success.started, 'success'); } catch (e) { addToast(e.message, 'error'); } finally { @@ -88,78 +90,94 @@ -
-

Dataset Column Mapper

-
-
-
- - -
-
- - -
-
- -
- -
- - -
-
- - {#if source === 'postgres'} -
+
+ +
+
- - + -
-
- - -
+
+
- {:else} -
- - -
- {/if} -
- +
+ +
+ + +
+
+ + {#if source === 'postgres'} +
+
+ +
+
+ +
+
+
+ {:else} +
+ +
+ {/if} + +
+ +
-
+
\ No newline at end of file diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index bc552f0..c950422 100755 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -95,7 +95,10 @@ async function requestApi(endpoint, method = 'GET', body = null) { const response = await fetch(`${API_BASE_URL}${endpoint}`, options); if (!response.ok) { const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.detail || `API request failed with status ${response.status}`); + const message = errorData.detail + ? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) + : `API request failed with status ${response.status}`; + throw new Error(message); } return await response.json(); } catch (error) { @@ -132,6 +135,7 @@ export const api = { // [/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; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 9e0b08d..1fa3320 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -23,16 +23,8 @@ */ function selectPlugin(plugin) { console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`); - if (plugin.id === 'superset-migration') { - goto('/migration'); - } else if (plugin.id === 'git-integration') { - goto('/git'); - } else if (plugin.id === 'superset-backup') { - goto('/tools/backups'); - } else if (plugin.id === 'superset-storage') { - goto('/tools/storage'); - } else if (plugin.id === 'superset-mapper') { - goto('/tools/mapper'); + if (plugin.ui_route) { + goto(plugin.ui_route); } else { selectedPlugin.set(plugin); } diff --git a/frontend/src/routes/tools/storage/+page.svelte b/frontend/src/routes/tools/storage/+page.svelte index 294dfe0..1dfb8f6 100644 --- a/frontend/src/routes/tools/storage/+page.svelte +++ b/frontend/src/routes/tools/storage/+page.svelte @@ -13,6 +13,7 @@