Compare commits
4 Commits
014-file-s
...
015-fronte
| Author | SHA1 | Date | |
|---|---|---|---|
| d10c23e658 | |||
| 1042b35d1b | |||
| 16ffeb1ed6 | |||
| da34deac02 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -66,7 +66,4 @@ backend/mappings.db
|
|||||||
|
|
||||||
|
|
||||||
backend/tasks.db
|
backend/tasks.db
|
||||||
|
backend/logs
|
||||||
# Git Integration repositories
|
|
||||||
backend/git_repos/
|
|
||||||
backend/backend/git_repos
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ Auto-generated from all feature plans. Last updated: 2025-12-19
|
|||||||
- LocalStorage (for language preference) (013-unify-frontend-css)
|
- LocalStorage (for language preference) (013-unify-frontend-css)
|
||||||
- Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit (Frontend) (014-file-storage-ui)
|
- Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit (Frontend) (014-file-storage-ui)
|
||||||
- Local Filesystem (for artifacts), Config (for storage path) (014-file-storage-ui)
|
- Local Filesystem (for artifacts), Config (for storage path) (014-file-storage-ui)
|
||||||
|
- Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit + Tailwind CSS (Frontend) (015-frontend-nav-redesign)
|
||||||
|
- N/A (UI reorganization and API integration) (015-frontend-nav-redesign)
|
||||||
|
|
||||||
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
|
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
|
||||||
|
|
||||||
@@ -49,9 +51,9 @@ cd src; pytest; ruff check .
|
|||||||
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
|
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 015-frontend-nav-redesign: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit + Tailwind CSS (Frontend)
|
||||||
- 014-file-storage-ui: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit (Frontend)
|
- 014-file-storage-ui: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit (Frontend)
|
||||||
- 013-unify-frontend-css: Added Node.js 18+ (Frontend Build), Svelte 5.x + SvelteKit, Tailwind CSS, `date-fns` (existing)
|
- 013-unify-frontend-css: Added Node.js 18+ (Frontend Build), Svelte 5.x + SvelteKit, Tailwind CSS, `date-fns` (existing)
|
||||||
- 011-git-integration-dashboard: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, GitPython (or CLI git), Pydantic, SQLAlchemy, Superset API
|
|
||||||
|
|
||||||
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<!--
|
<!--
|
||||||
SYNC IMPACT REPORT
|
SYNC IMPACT REPORT
|
||||||
Version: 1.7.1 (Simplified Workflow)
|
Version: 1.8.0 (Frontend Unification)
|
||||||
Changes:
|
Changes:
|
||||||
- Simplified Generation Workflow to a single phase: Code Generation from `tasks.md`.
|
- Added Principle VIII: Unified Frontend Experience (Mandating Design System & i18n).
|
||||||
- Removed multi-phase Architecture/Implementation split to streamline development.
|
|
||||||
Templates Status:
|
Templates Status:
|
||||||
- .specify/templates/plan-template.md: ✅ Aligned (Dynamic check).
|
- .specify/templates/plan-template.md: ✅ Aligned.
|
||||||
- .specify/templates/spec-template.md: ✅ Aligned.
|
- .specify/templates/spec-template.md: ✅ Aligned.
|
||||||
- .specify/templates/tasks-template.md: ✅ Aligned.
|
- .specify/templates/tasks-template.md: ✅ Aligned.
|
||||||
-->
|
-->
|
||||||
@@ -37,6 +36,11 @@ To maintain semantic coherence, code must adhere to the complexity limits (Modul
|
|||||||
### VII. Everything is a Plugin
|
### VII. Everything is a Plugin
|
||||||
All functional extensions, tools, or major features must be implemented as modular Plugins inheriting from `PluginBase`. Logic should not reside in standalone services or scripts unless strictly necessary for core infrastructure. This ensures a unified execution model via the `TaskManager`, consistent logging, and modularity.
|
All functional extensions, tools, or major features must be implemented as modular Plugins inheriting from `PluginBase`. Logic should not reside in standalone services or scripts unless strictly necessary for core infrastructure. This ensures a unified execution model via the `TaskManager`, consistent logging, and modularity.
|
||||||
|
|
||||||
|
### VIII. Unified Frontend Experience
|
||||||
|
To ensure a consistent and accessible user experience, all frontend implementations must strictly adhere to the unified design and localization standards.
|
||||||
|
- **Component Reusability**: All UI elements MUST utilize the standardized Svelte component library (`src/lib/ui`) and centralized design tokens. Ad-hoc styling and hardcoded values are prohibited.
|
||||||
|
- **Internationalization (i18n)**: All user-facing text MUST be extracted to the translation system (`src/lib/i18n`). Hardcoded strings in the UI are prohibited.
|
||||||
|
|
||||||
## File Structure Standards
|
## File Structure Standards
|
||||||
Refer to **Section III (File Structure Standard)** in `semantic_protocol.md` for the authoritative definitions of:
|
Refer to **Section III (File Structure Standard)** in `semantic_protocol.md` for the authoritative definitions of:
|
||||||
- Python Module Headers (`.py`)
|
- Python Module Headers (`.py`)
|
||||||
@@ -64,4 +68,4 @@ This Constitution establishes the "Semantic Code Generation Protocol" as the sup
|
|||||||
- **Amendments**: Changes to core principles require a Constitution amendment. Changes to technical syntax require a Protocol update.
|
- **Amendments**: Changes to core principles require a Constitution amendment. Changes to technical syntax require a Protocol update.
|
||||||
- **Compliance**: Failure to adhere to the Protocol constitutes a build failure.
|
- **Compliance**: Failure to adhere to the Protocol constitutes a build failure.
|
||||||
|
|
||||||
**Version**: 1.7.1 | **Ratified**: 2025-12-19 | **Last Amended**: 2026-01-13
|
**Version**: 1.8.0 | **Ratified**: 2025-12-19 | **Last Amended**: 2026-01-26
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ router = APIRouter()
|
|||||||
# [DEF:ScheduleSchema:DataClass]
|
# [DEF:ScheduleSchema:DataClass]
|
||||||
class ScheduleSchema(BaseModel):
|
class ScheduleSchema(BaseModel):
|
||||||
enabled: bool = False
|
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:ScheduleSchema:DataClass]
|
||||||
|
|
||||||
# [DEF:EnvironmentResponse:DataClass]
|
# [DEF:EnvironmentResponse:DataClass]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any, Optional
|
||||||
from .logger import belief_scope
|
from .logger import belief_scope
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -68,6 +68,21 @@ class PluginBase(ABC):
|
|||||||
pass
|
pass
|
||||||
# [/DEF:version:Function]
|
# [/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
|
@abstractmethod
|
||||||
# [DEF:get_schema:Function]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Returns the JSON schema for the plugin's input parameters.
|
# @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")
|
name: str = Field(..., description="Human-readable name for the plugin")
|
||||||
description: str = Field(..., description="Brief description of what the plugin does")
|
description: str = Field(..., description="Brief description of what the plugin does")
|
||||||
version: str = Field(..., description="Version of the plugin")
|
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")
|
input_schema: Dict[str, Any] = Field(..., description="JSON schema for input parameters", alias="schema")
|
||||||
# [/DEF:PluginConfig:Class]
|
# [/DEF:PluginConfig:Class]
|
||||||
@@ -141,6 +141,7 @@ class PluginLoader:
|
|||||||
name=plugin_instance.name,
|
name=plugin_instance.name,
|
||||||
description=plugin_instance.description,
|
description=plugin_instance.description,
|
||||||
version=plugin_instance.version,
|
version=plugin_instance.version,
|
||||||
|
ui_route=plugin_instance.ui_route,
|
||||||
schema=schema,
|
schema=schema,
|
||||||
)
|
)
|
||||||
# The following line is commented out because it requires a schema to be passed to validate against.
|
# The following line is commented out because it requires a schema to be passed to validate against.
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ class BackupPlugin(PluginBase):
|
|||||||
return "1.0.0"
|
return "1.0.0"
|
||||||
# [/DEF:version:Function]
|
# [/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]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Returns the JSON schema for backup plugin parameters.
|
# @PURPOSE: Returns the JSON schema for backup plugin parameters.
|
||||||
# @PRE: Plugin instance exists.
|
# @PRE: Plugin instance exists.
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ class DebugPlugin(PluginBase):
|
|||||||
return "1.0.0"
|
return "1.0.0"
|
||||||
# [/DEF:version:Function]
|
# [/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]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Returns the JSON schema for the debug plugin parameters.
|
# @PURPOSE: Returns the JSON schema for the debug plugin parameters.
|
||||||
# @PRE: Plugin instance exists.
|
# @PRE: Plugin instance exists.
|
||||||
|
|||||||
@@ -99,6 +99,15 @@ class GitPlugin(PluginBase):
|
|||||||
return "0.1.0"
|
return "0.1.0"
|
||||||
# [/DEF:version:Function]
|
# [/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]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина.
|
# @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина.
|
||||||
# @PRE: GitPlugin is initialized.
|
# @PRE: GitPlugin is initialized.
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ class MapperPlugin(PluginBase):
|
|||||||
return "1.0.0"
|
return "1.0.0"
|
||||||
# [/DEF:version:Function]
|
# [/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]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Returns the JSON schema for the mapper plugin parameters.
|
# @PURPOSE: Returns the JSON schema for the mapper plugin parameters.
|
||||||
# @PRE: Plugin instance exists.
|
# @PRE: Plugin instance exists.
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ class MigrationPlugin(PluginBase):
|
|||||||
return "1.0.0"
|
return "1.0.0"
|
||||||
# [/DEF:version:Function]
|
# [/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]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Returns the JSON schema for migration plugin parameters.
|
# @PURPOSE: Returns the JSON schema for migration plugin parameters.
|
||||||
# @PRE: Config manager is available.
|
# @PRE: Config manager is available.
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ class SearchPlugin(PluginBase):
|
|||||||
return "1.0.0"
|
return "1.0.0"
|
||||||
# [/DEF:version:Function]
|
# [/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]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Returns the JSON schema for the search plugin parameters.
|
# @PURPOSE: Returns the JSON schema for the search plugin parameters.
|
||||||
# @PRE: Plugin instance exists.
|
# @PRE: Plugin instance exists.
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ class StoragePlugin(PluginBase):
|
|||||||
return "1.0.0"
|
return "1.0.0"
|
||||||
# [/DEF:version:Function]
|
# [/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]
|
# [DEF:get_schema:Function]
|
||||||
# @PURPOSE: Returns the JSON schema for storage plugin parameters.
|
# @PURPOSE: Returns the JSON schema for storage plugin parameters.
|
||||||
# @PRE: None.
|
# @PRE: None.
|
||||||
|
|||||||
BIN
backend/tasks.db
BIN
backend/tasks.db
Binary file not shown.
@@ -25,35 +25,12 @@
|
|||||||
>
|
>
|
||||||
{$t.nav.dashboard}
|
{$t.nav.dashboard}
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
href="/migration"
|
|
||||||
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/migration') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
|
|
||||||
>
|
|
||||||
{$t.nav.migration}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/git"
|
|
||||||
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/git') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
|
|
||||||
>
|
|
||||||
{$t.nav.git}
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
href="/tasks"
|
href="/tasks"
|
||||||
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/tasks') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
|
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/tasks') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
|
||||||
>
|
>
|
||||||
{$t.nav.tasks}
|
{$t.nav.tasks}
|
||||||
</a>
|
</a>
|
||||||
<div class="relative inline-block group">
|
|
||||||
<button class="text-gray-600 hover:text-blue-600 font-medium pb-1 {$page.url.pathname.startsWith('/tools') ? 'text-blue-600 border-b-2 border-blue-600' : ''}">
|
|
||||||
{$t.nav.tools}
|
|
||||||
</button>
|
|
||||||
<div class="absolute hidden group-hover:block bg-white shadow-lg rounded-md mt-1 py-2 w-48 z-10 border border-gray-100 before:absolute before:-top-2 before:left-0 before:right-0 before:h-2 before:content-[''] right-0">
|
|
||||||
<a href="/tools/search" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.tools_search}</a>
|
|
||||||
<a href="/tools/mapper" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.tools_mapper}</a>
|
|
||||||
<a href="/tools/debug" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.tools_debug}</a>
|
|
||||||
<a href="/tools/storage" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.tools_storage}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="relative inline-block group">
|
<div class="relative inline-block group">
|
||||||
<button class="text-gray-600 hover:text-blue-600 font-medium pb-1 {$page.url.pathname.startsWith('/settings') ? 'text-blue-600 border-b-2 border-blue-600' : ''}">
|
<button class="text-gray-600 hover:text-blue-600 font-medium pb-1 {$page.url.pathname.startsWith('/settings') ? 'text-blue-600 border-b-2 border-blue-600' : ''}">
|
||||||
{$t.nav.settings}
|
{$t.nav.settings}
|
||||||
@@ -62,7 +39,6 @@
|
|||||||
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_general}</a>
|
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_general}</a>
|
||||||
<a href="/settings/connections" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_connections}</a>
|
<a href="/settings/connections" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_connections}</a>
|
||||||
<a href="/settings/git" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_git}</a>
|
<a href="/settings/git" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_git}</a>
|
||||||
<a href="/settings/environments" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_environments}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|||||||
84
frontend/src/components/backups/BackupList.svelte
Normal file
84
frontend/src/components/backups/BackupList.svelte
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<!-- [DEF:BackupList:Component] -->
|
||||||
|
<!--
|
||||||
|
@SEMANTICS: backup, list, table
|
||||||
|
@PURPOSE: Displays a list of existing backups.
|
||||||
|
@LAYER: Component
|
||||||
|
@RELATION: USED_BY -> frontend/src/components/backups/BackupManager.svelte
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// [SECTION: IMPORTS]
|
||||||
|
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]
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
|
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
{$t.storage.table.name}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
{$t.tasks.target_env}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
{$t.storage.table.created_at}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
{$t.storage.table.actions}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
{#each backups as backup}
|
||||||
|
<tr class="hover:bg-gray-50 transition-colors">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{backup.name}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{backup.environment}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{new Date(backup.created_at).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="text-blue-600 hover:text-blue-900"
|
||||||
|
on:click={() => goto(`/tools/storage?path=backups/${backup.name}`)}
|
||||||
|
>
|
||||||
|
{$t.storage.table.go_to_storage}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-6 py-10 text-center text-gray-500">
|
||||||
|
{$t.storage.no_files}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- [/DEF:BackupList:Component] -->
|
||||||
241
frontend/src/components/backups/BackupManager.svelte
Normal file
241
frontend/src/components/backups/BackupManager.svelte
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<!-- [DEF:BackupManager:Component] -->
|
||||||
|
<!--
|
||||||
|
@SEMANTICS: backup, manager, orchestrator
|
||||||
|
@PURPOSE: Main container for backup management, handling creation and listing.
|
||||||
|
@LAYER: Feature
|
||||||
|
@RELATION: USES -> BackupList
|
||||||
|
@RELATION: USES -> api
|
||||||
|
|
||||||
|
@INVARIANT: Only one backup task can be triggered at a time from the UI.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// [SECTION: IMPORTS]
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from '../../lib/i18n';
|
||||||
|
import { api, requestApi } from '../../lib/api';
|
||||||
|
import { addToast } from '../../lib/toasts';
|
||||||
|
import { Button, Card, Select, Input } from '../../lib/ui';
|
||||||
|
import BackupList from './BackupList.svelte';
|
||||||
|
import type { Backup } from '../../types/backup';
|
||||||
|
// [/SECTION]
|
||||||
|
|
||||||
|
// [SECTION: STATE]
|
||||||
|
let backups: Backup[] = [];
|
||||||
|
let environments: any[] = [];
|
||||||
|
let selectedEnvId = '';
|
||||||
|
let loading = true;
|
||||||
|
let creating = false;
|
||||||
|
let savingSchedule = false;
|
||||||
|
|
||||||
|
// Schedule state for selected environment
|
||||||
|
let scheduleEnabled = false;
|
||||||
|
let cronExpression = '0 0 * * *';
|
||||||
|
|
||||||
|
$: selectedEnv = environments.find(e => e.id === selectedEnvId);
|
||||||
|
$: if (selectedEnv) {
|
||||||
|
scheduleEnabled = selectedEnv.backup_schedule?.enabled ?? false;
|
||||||
|
cronExpression = selectedEnv.backup_schedule?.cron_expression ?? '0 0 * * *';
|
||||||
|
}
|
||||||
|
// [/SECTION]
|
||||||
|
|
||||||
|
// [DEF:loadData:Function]
|
||||||
|
/**
|
||||||
|
* @purpose Loads backups and environments from the backend.
|
||||||
|
*
|
||||||
|
* @pre API must be reachable.
|
||||||
|
* @post environments and backups stores are populated.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @side_effect Updates local state variables.
|
||||||
|
*/
|
||||||
|
// @RELATION: CALLS -> api.getEnvironmentsList
|
||||||
|
// @RELATION: CALLS -> api.requestApi
|
||||||
|
async function loadData() {
|
||||||
|
console.log("[BackupManager][Entry] Loading data.");
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const [envsData, storageData] = await Promise.all([
|
||||||
|
api.getEnvironmentsList(),
|
||||||
|
requestApi('/storage/files?category=backups')
|
||||||
|
]);
|
||||||
|
environments = envsData;
|
||||||
|
|
||||||
|
// Map storage files to Backup type
|
||||||
|
backups = (storageData || []).map((file: any) => ({
|
||||||
|
id: file.name,
|
||||||
|
name: file.name,
|
||||||
|
environment: file.path.split('/')[0] || 'Unknown',
|
||||||
|
created_at: file.created_at,
|
||||||
|
size_bytes: file.size,
|
||||||
|
status: 'success'
|
||||||
|
}));
|
||||||
|
console.log("[BackupManager][Action] Data loaded successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[BackupManager][Coherence:Failed] Load failed", error);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [/DEF:loadData:Function]
|
||||||
|
|
||||||
|
// [DEF:handleCreateBackup:Function]
|
||||||
|
/**
|
||||||
|
* @purpose Triggers a new backup task for the selected environment.
|
||||||
|
*
|
||||||
|
* @pre selectedEnvId must be a valid environment ID.
|
||||||
|
* @post A new task is created on the backend.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @side_effect Dispatches a toast notification.
|
||||||
|
*/
|
||||||
|
// @RELATION: CALLS -> api.createTask
|
||||||
|
// [DEF:handleUpdateSchedule:Function]
|
||||||
|
/**
|
||||||
|
* @purpose Updates the backup schedule for the selected environment.
|
||||||
|
* @pre selectedEnvId must be set.
|
||||||
|
* @post Environment config is updated on the backend.
|
||||||
|
*/
|
||||||
|
async function handleUpdateSchedule() {
|
||||||
|
if (!selectedEnvId) return;
|
||||||
|
|
||||||
|
console.log(`[BackupManager][Action] Updating schedule for env: ${selectedEnvId}`);
|
||||||
|
savingSchedule = true;
|
||||||
|
try {
|
||||||
|
await api.updateEnvironmentSchedule(selectedEnvId, {
|
||||||
|
enabled: scheduleEnabled,
|
||||||
|
cron_expression: cronExpression
|
||||||
|
});
|
||||||
|
addToast($t.common.success, 'success');
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
environments = environments.map(e =>
|
||||||
|
e.id === selectedEnvId
|
||||||
|
? { ...e, backup_schedule: { enabled: scheduleEnabled, cron_expression: cronExpression } }
|
||||||
|
: e
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[BackupManager][Coherence:Failed] Schedule update failed", error);
|
||||||
|
} finally {
|
||||||
|
savingSchedule = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [/DEF:handleUpdateSchedule:Function]
|
||||||
|
|
||||||
|
async function handleCreateBackup() {
|
||||||
|
if (!selectedEnvId) {
|
||||||
|
addToast($t.tasks.select_env, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BackupManager][Action] Triggering backup for env: ${selectedEnvId}`);
|
||||||
|
creating = true;
|
||||||
|
try {
|
||||||
|
await api.createTask('superset-backup', { environment_id: selectedEnvId });
|
||||||
|
addToast($t.common.success, 'success');
|
||||||
|
console.log("[BackupManager][Coherence:OK] Backup task triggered.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[BackupManager][Coherence:Failed] Create failed", error);
|
||||||
|
} finally {
|
||||||
|
creating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [/DEF:handleCreateBackup:Function]
|
||||||
|
|
||||||
|
onMount(loadData);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<Card title={$t.tasks.manual_backup}>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-end gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<Select
|
||||||
|
label={$t.tasks.target_env}
|
||||||
|
bind:value={selectedEnvId}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: $t.tasks.select_env },
|
||||||
|
...environments.map(e => ({ value: e.id, label: e.name }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
on:click={handleCreateBackup}
|
||||||
|
disabled={creating || !selectedEnvId}
|
||||||
|
>
|
||||||
|
{creating ? $t.common.loading : $t.tasks.start_backup}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedEnvId}
|
||||||
|
<div class="pt-6 border-t border-gray-100 mt-4">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
{$t.tasks.backup_schedule}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-start gap-6">
|
||||||
|
<div class="pt-8">
|
||||||
|
<label class="flex items-center gap-3 cursor-pointer group">
|
||||||
|
<div class="relative inline-flex items-center">
|
||||||
|
<input type="checkbox" bind:checked={scheduleEnabled} class="sr-only peer" />
|
||||||
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-gray-900 transition-colors">{$t.tasks.schedule_enabled}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 space-y-2">
|
||||||
|
<Input
|
||||||
|
label={$t.tasks.cron_label}
|
||||||
|
placeholder="0 0 * * *"
|
||||||
|
bind:value={cronExpression}
|
||||||
|
disabled={!scheduleEnabled}
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 italic">{$t.tasks.cron_hint}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-8">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
on:click={handleUpdateSchedule}
|
||||||
|
disabled={savingSchedule}
|
||||||
|
class="min-w-[100px]"
|
||||||
|
>
|
||||||
|
{#if savingSchedule}
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
{$t.common.loading}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
{$t.common.save}
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700">{$t.storage.backups}</h2>
|
||||||
|
{#if loading}
|
||||||
|
<div class="py-10 text-center text-gray-500">{$t.common.loading}</div>
|
||||||
|
{:else}
|
||||||
|
<BackupList {backups} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
|
<!-- [/DEF:BackupManager:Component] -->
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
import { getConnections } from '../../services/connectionService.js';
|
import { getConnections } from '../../services/connectionService.js';
|
||||||
import { selectedTask } from '../../lib/stores.js';
|
import { selectedTask } from '../../lib/stores.js';
|
||||||
import { addToast } from '../../lib/toasts.js';
|
import { addToast } from '../../lib/toasts.js';
|
||||||
|
import { t } from '../../lib/i18n';
|
||||||
|
import { Button, Card, Select, Input } from '../../lib/ui';
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
let envs = [];
|
let envs = [];
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
envs = await envsRes.json();
|
envs = await envsRes.json();
|
||||||
connections = await getConnections();
|
connections = await getConnections();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast('Failed to fetch data', 'error');
|
addToast($t.mapper.errors.fetch_failed, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:fetchData:Function]
|
// [/DEF:fetchData:Function]
|
||||||
@@ -47,17 +49,17 @@
|
|||||||
// @POST: Mapper task is started and selectedTask is updated.
|
// @POST: Mapper task is started and selectedTask is updated.
|
||||||
async function handleRunMapper() {
|
async function handleRunMapper() {
|
||||||
if (!selectedEnv || !datasetId) {
|
if (!selectedEnv || !datasetId) {
|
||||||
addToast('Please fill in required fields', 'warning');
|
addToast($t.mapper.errors.required_fields, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source === 'postgres' && (!selectedConnection || !tableName)) {
|
if (source === 'postgres' && (!selectedConnection || !tableName)) {
|
||||||
addToast('Connection and Table Name are required for postgres source', 'warning');
|
addToast($t.mapper.errors.postgres_required, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source === 'excel' && !excelPath) {
|
if (source === 'excel' && !excelPath) {
|
||||||
addToast('Excel path is required for excel source', 'warning');
|
addToast($t.mapper.errors.excel_required, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
selectedTask.set(task);
|
selectedTask.set(task);
|
||||||
addToast('Mapper task started', 'success');
|
addToast($t.mapper.success.started, 'success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast(e.message, 'error');
|
addToast(e.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -88,35 +90,39 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div class="space-y-6">
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Dataset Column Mapper</h3>
|
<Card title={$t.mapper.title}>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="mapper-env" class="block text-sm font-medium text-gray-700">Environment</label>
|
<Select
|
||||||
<select id="mapper-env" bind:value={selectedEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
label={$t.mapper.environment}
|
||||||
<option value="" disabled>-- Select Environment --</option>
|
bind:value={selectedEnv}
|
||||||
{#each envs as env}
|
options={[
|
||||||
<option value={env.id}>{env.name}</option>
|
{ value: '', label: $t.mapper.select_env },
|
||||||
{/each}
|
...envs.map(e => ({ value: e.id, label: e.name }))
|
||||||
</select>
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="mapper-ds-id" class="block text-sm font-medium text-gray-700">Dataset ID</label>
|
<Input
|
||||||
<input type="number" id="mapper-ds-id" bind:value={datasetId} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
label={$t.mapper.dataset_id}
|
||||||
|
type="number"
|
||||||
|
bind:value={datasetId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">Mapping Source</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">{$t.mapper.source}</label>
|
||||||
<div class="mt-2 flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<label class="inline-flex items-center">
|
<label class="inline-flex items-center">
|
||||||
<input type="radio" bind:group={source} value="postgres" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
|
<input type="radio" bind:group={source} value="postgres" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" />
|
||||||
<span class="ml-2 text-sm text-gray-700">PostgreSQL</span>
|
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_postgres}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="inline-flex items-center">
|
<label class="inline-flex items-center">
|
||||||
<input type="radio" bind:group={source} value="excel" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
|
<input type="radio" bind:group={source} value="excel" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" />
|
||||||
<span class="ml-2 text-sm text-gray-700">Excel</span>
|
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_excel}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,42 +130,54 @@
|
|||||||
{#if source === 'postgres'}
|
{#if source === 'postgres'}
|
||||||
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
|
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
|
||||||
<div>
|
<div>
|
||||||
<label for="mapper-conn" class="block text-sm font-medium text-gray-700">Saved Connection</label>
|
<Select
|
||||||
<select id="mapper-conn" bind:value={selectedConnection} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
label={$t.mapper.connection}
|
||||||
<option value="" disabled>-- Select Connection --</option>
|
bind:value={selectedConnection}
|
||||||
{#each connections as conn}
|
options={[
|
||||||
<option value={conn.id}>{conn.name}</option>
|
{ value: '', label: $t.mapper.select_connection },
|
||||||
{/each}
|
...connections.map(c => ({ value: c.id, label: c.name }))
|
||||||
</select>
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="mapper-table" class="block text-sm font-medium text-gray-700">Table Name</label>
|
<Input
|
||||||
<input type="text" id="mapper-table" bind:value={tableName} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
label={$t.mapper.table_name}
|
||||||
|
type="text"
|
||||||
|
bind:value={tableName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="mapper-schema" class="block text-sm font-medium text-gray-700">Table Schema</label>
|
<Input
|
||||||
<input type="text" id="mapper-schema" bind:value={tableSchema} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
label={$t.mapper.table_schema}
|
||||||
|
type="text"
|
||||||
|
bind:value={tableSchema}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
|
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
|
||||||
<label for="mapper-excel" class="block text-sm font-medium text-gray-700">Excel File Path</label>
|
<Input
|
||||||
<input type="text" id="mapper-excel" bind:value={excelPath} placeholder="/path/to/mapping.xlsx" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
label={$t.mapper.excel_path}
|
||||||
|
type="text"
|
||||||
|
bind:value={excelPath}
|
||||||
|
placeholder="/path/to/mapping.xlsx"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end pt-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
on:click={handleRunMapper}
|
on:click={handleRunMapper}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
{isRunning ? 'Starting...' : 'Run Mapper'}
|
{isRunning ? $t.mapper.starting : $t.mapper.run}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
<!-- [/DEF:MapperTool:Component] -->
|
<!-- [/DEF:MapperTool:Component] -->
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
<!-- [DEF:SearchTool:Component] -->
|
|
||||||
<!--
|
|
||||||
@SEMANTICS: search, tool, dataset, regex
|
|
||||||
@PURPOSE: UI component for searching datasets using the SearchPlugin.
|
|
||||||
@LAYER: UI
|
|
||||||
@RELATION: USES -> frontend/src/services/toolsService.js
|
|
||||||
-->
|
|
||||||
<script>
|
|
||||||
// [SECTION: IMPORTS]
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { runTask, getTaskStatus } from '../../services/toolsService.js';
|
|
||||||
import { selectedTask } from '../../lib/stores.js';
|
|
||||||
import { addToast } from '../../lib/toasts.js';
|
|
||||||
// [/SECTION]
|
|
||||||
|
|
||||||
let envs = [];
|
|
||||||
let selectedEnv = '';
|
|
||||||
let searchQuery = '';
|
|
||||||
let isRunning = false;
|
|
||||||
let results = null;
|
|
||||||
let pollInterval;
|
|
||||||
|
|
||||||
// [DEF:fetchEnvironments:Function]
|
|
||||||
// @PURPOSE: Fetches the list of available environments.
|
|
||||||
// @PRE: None.
|
|
||||||
// @POST: envs array is populated.
|
|
||||||
async function fetchEnvironments() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/environments');
|
|
||||||
envs = await res.json();
|
|
||||||
} catch (e) {
|
|
||||||
addToast('Failed to fetch environments', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [/DEF:fetchEnvironments:Function]
|
|
||||||
|
|
||||||
// [DEF:handleSearch:Function]
|
|
||||||
// @PURPOSE: Triggers the SearchPlugin task.
|
|
||||||
// @PRE: selectedEnv and searchQuery must be set.
|
|
||||||
// @POST: Task is started and polling begins.
|
|
||||||
async function handleSearch() {
|
|
||||||
if (!selectedEnv || !searchQuery) {
|
|
||||||
addToast('Please select environment and enter query', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRunning = true;
|
|
||||||
results = null;
|
|
||||||
try {
|
|
||||||
// Find the environment name from ID
|
|
||||||
const env = envs.find(e => e.id === selectedEnv);
|
|
||||||
const task = await runTask('search-datasets', {
|
|
||||||
env: env.name,
|
|
||||||
query: searchQuery
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedTask.set(task);
|
|
||||||
startPolling(task.id);
|
|
||||||
} catch (e) {
|
|
||||||
isRunning = false;
|
|
||||||
addToast(e.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [/DEF:handleSearch:Function]
|
|
||||||
|
|
||||||
// [DEF:startPolling:Function]
|
|
||||||
// @PURPOSE: Polls for task completion and results.
|
|
||||||
// @PRE: taskId is provided.
|
|
||||||
// @POST: pollInterval is set and results are updated on success.
|
|
||||||
function startPolling(taskId) {
|
|
||||||
if (pollInterval) clearInterval(pollInterval);
|
|
||||||
|
|
||||||
pollInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const task = await getTaskStatus(taskId);
|
|
||||||
selectedTask.set(task);
|
|
||||||
|
|
||||||
if (task.status === 'SUCCESS') {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
isRunning = false;
|
|
||||||
results = task.result;
|
|
||||||
addToast('Search completed', 'success');
|
|
||||||
} else if (task.status === 'FAILED') {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
isRunning = false;
|
|
||||||
addToast('Search failed', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
isRunning = false;
|
|
||||||
addToast('Error polling task status', 'error');
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
// [/DEF:startPolling:Function]
|
|
||||||
|
|
||||||
onMount(fetchEnvironments);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Search Dataset Metadata</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-end">
|
|
||||||
<div>
|
|
||||||
<label for="env-select" class="block text-sm font-medium text-gray-700">Environment</label>
|
|
||||||
<select
|
|
||||||
id="env-select"
|
|
||||||
bind:value={selectedEnv}
|
|
||||||
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
|
||||||
>
|
|
||||||
<option value="" disabled>-- Select Environment --</option>
|
|
||||||
{#each envs as env}
|
|
||||||
<option value={env.id}>{env.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="search-query" class="block text-sm font-medium text-gray-700">Regex Pattern</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="search-query"
|
|
||||||
bind:value={searchQuery}
|
|
||||||
placeholder="e.g. from dm.*\.account"
|
|
||||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex justify-end">
|
|
||||||
<button
|
|
||||||
on:click={handleSearch}
|
|
||||||
disabled={isRunning}
|
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{#if isRunning}
|
|
||||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Searching...
|
|
||||||
{:else}
|
|
||||||
Search
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if results}
|
|
||||||
<div class="bg-white shadow overflow-hidden sm:rounded-md border border-gray-200">
|
|
||||||
<div class="px-4 py-5 sm:px-6 flex justify-between items-center bg-gray-50 border-b border-gray-200">
|
|
||||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Search Results
|
|
||||||
</h3>
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
||||||
{results.count} matches
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul class="divide-y divide-gray-200">
|
|
||||||
{#each results.results as item}
|
|
||||||
<li class="p-4 hover:bg-gray-50">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="text-sm font-medium text-indigo-600 truncate">
|
|
||||||
{item.dataset_name} (ID: {item.dataset_id})
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 flex-shrink-0 flex">
|
|
||||||
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
|
||||||
Field: {item.field}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<pre class="text-xs text-gray-500 bg-gray-50 p-2 rounded border border-gray-100 overflow-x-auto">{item.match_context}</pre>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
{#if results.count === 0}
|
|
||||||
<li class="p-8 text-center text-gray-500 italic">
|
|
||||||
No matches found for the given pattern.
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<!-- [/SECTION] -->
|
|
||||||
<!-- [/DEF:SearchTool:Component] -->
|
|
||||||
@@ -95,7 +95,10 @@ async function requestApi(endpoint, method = 'GET', body = null) {
|
|||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
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();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -132,6 +135,7 @@ export const api = {
|
|||||||
// [/DEF:api_module:Module]
|
// [/DEF:api_module:Module]
|
||||||
|
|
||||||
// Export individual functions for easier use in components
|
// Export individual functions for easier use in components
|
||||||
|
export { requestApi };
|
||||||
export const getPlugins = api.getPlugins;
|
export const getPlugins = api.getPlugins;
|
||||||
export const getTasks = api.getTasks;
|
export const getTasks = api.getTasks;
|
||||||
export const getTask = api.getTask;
|
export const getTask = api.getTask;
|
||||||
|
|||||||
@@ -23,10 +23,8 @@
|
|||||||
*/
|
*/
|
||||||
function selectPlugin(plugin) {
|
function selectPlugin(plugin) {
|
||||||
console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`);
|
console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`);
|
||||||
if (plugin.id === 'superset-migration') {
|
if (plugin.ui_route) {
|
||||||
goto('/migration');
|
goto(plugin.ui_route);
|
||||||
} else if (plugin.id === 'git-integration') {
|
|
||||||
goto('/git');
|
|
||||||
} else {
|
} else {
|
||||||
selectedPlugin.set(plugin);
|
selectedPlugin.set(plugin);
|
||||||
}
|
}
|
||||||
@@ -82,7 +80,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{#each data.plugins as plugin}
|
{#each data.plugins.filter(p => p.id !== 'superset-search') as plugin}
|
||||||
<div
|
<div
|
||||||
on:click={() => selectPlugin(plugin)}
|
on:click={() => selectPlugin(plugin)}
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { gitService } from '../../../services/gitService';
|
|
||||||
import { addToast as toast } from '../../../lib/toasts.js';
|
|
||||||
|
|
||||||
let environments = [];
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
environments = await gitService.getEnvironments();
|
|
||||||
} catch (e) {
|
|
||||||
toast(e.message, 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="p-6">
|
|
||||||
<h1 class="text-2xl font-bold mb-6">Deployment Environments</h1>
|
|
||||||
|
|
||||||
<div class="bg-white p-6 rounded shadow">
|
|
||||||
<h2 class="text-xl font-semibold mb-4">Target Environments</h2>
|
|
||||||
{#if environments.length === 0}
|
|
||||||
<p class="text-gray-500">No deployment environments configured.</p>
|
|
||||||
{:else}
|
|
||||||
<ul class="divide-y">
|
|
||||||
{#each environments as env}
|
|
||||||
<li class="py-3 flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<span class="font-medium">{env.name}</span>
|
|
||||||
<div class="text-xs text-gray-400">{env.superset_url}</div>
|
|
||||||
</div>
|
|
||||||
<span class="px-2 py-1 text-xs rounded {env.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}">
|
|
||||||
{env.is_active ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -116,13 +116,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-4 max-w-6xl">
|
<div class="container mx-auto p-4 max-w-6xl">
|
||||||
<PageHeader title={$t.tasks.management}>
|
<PageHeader title={$t.tasks.management} />
|
||||||
<div slot="actions">
|
|
||||||
<Button on:click={() => showBackupModal = true}>
|
|
||||||
{$t.tasks.run_backup}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div class="lg:col-span-1">
|
<div class="lg:col-span-1">
|
||||||
|
|||||||
27
frontend/src/routes/tools/backups/+page.svelte
Normal file
27
frontend/src/routes/tools/backups/+page.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!-- [DEF:BackupPage:Component] -->
|
||||||
|
<!--
|
||||||
|
@SEMANTICS: backup, page, tools
|
||||||
|
@PURPOSE: Entry point for the Backup Management interface.
|
||||||
|
@LAYER: Page
|
||||||
|
@RELATION: USES -> BackupManager
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// [SECTION: IMPORTS]
|
||||||
|
import { t } from '../../../lib/i18n';
|
||||||
|
import { PageHeader } from '../../../lib/ui';
|
||||||
|
import BackupManager from '../../../components/backups/BackupManager.svelte';
|
||||||
|
// [/SECTION]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
|
<div class="container mx-auto p-4 max-w-6xl">
|
||||||
|
<PageHeader title={$t.nav.tools_backups} />
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<BackupManager />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
|
<!-- [/DEF:BackupPage:Component] -->
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<!-- [DEF:SearchPage:Component] -->
|
|
||||||
<!--
|
|
||||||
@SEMANTICS: search, page, tool
|
|
||||||
@PURPOSE: Page for the dataset search tool.
|
|
||||||
@LAYER: UI
|
|
||||||
-->
|
|
||||||
<script>
|
|
||||||
import SearchTool from '../../../components/tools/SearchTool.svelte';
|
|
||||||
import TaskRunner from '../../../components/TaskRunner.svelte';
|
|
||||||
import { PageHeader } from '$lib/ui';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto p-6">
|
|
||||||
<PageHeader title="Dataset Search" />
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<SearchTool />
|
|
||||||
</div>
|
|
||||||
<div class="lg:col-span-1">
|
|
||||||
<TaskRunner />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- [/DEF:SearchPage:Component] -->
|
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { listFiles, deleteFile } from '../../../services/storageService';
|
import { listFiles, deleteFile } from '../../../services/storageService';
|
||||||
import { addToast } from '../../../lib/toasts';
|
import { addToast } from '../../../lib/toasts';
|
||||||
import { t } from '../../../lib/i18n';
|
import { t } from '../../../lib/i18n';
|
||||||
@@ -90,6 +91,8 @@
|
|||||||
// [DEF:navigateUp:Function]
|
// [DEF:navigateUp:Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Navigates one level up in the directory structure.
|
* @purpose Navigates one level up in the directory structure.
|
||||||
|
* @pre currentPath is set and deeper than activeTab root.
|
||||||
|
* @post currentPath is moved up one directory level.
|
||||||
*/
|
*/
|
||||||
function navigateUp() {
|
function navigateUp() {
|
||||||
if (!currentPath || currentPath === activeTab) return;
|
if (!currentPath || currentPath === activeTab) return;
|
||||||
@@ -100,7 +103,18 @@
|
|||||||
}
|
}
|
||||||
// [/DEF:navigateUp:Function]
|
// [/DEF:navigateUp:Function]
|
||||||
|
|
||||||
onMount(loadFiles);
|
onMount(() => {
|
||||||
|
const pathParam = $page.url.searchParams.get('path');
|
||||||
|
if (pathParam) {
|
||||||
|
currentPath = pathParam;
|
||||||
|
if (pathParam.startsWith('repositorys')) {
|
||||||
|
activeTab = 'repositorys';
|
||||||
|
} else {
|
||||||
|
activeTab = 'backups';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadFiles();
|
||||||
|
});
|
||||||
|
|
||||||
$: if (activeTab) {
|
$: if (activeTab) {
|
||||||
// Reset path when switching tabs
|
// Reset path when switching tabs
|
||||||
|
|||||||
22
frontend/src/types/backup.ts
Normal file
22
frontend/src/types/backup.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* [DEF:BackupTypes:Module]
|
||||||
|
* @SEMANTICS: types, backup, interface
|
||||||
|
* @PURPOSE: Defines types and interfaces for the Backup Management UI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Backup {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
environment: string;
|
||||||
|
created_at: string;
|
||||||
|
size_bytes?: number;
|
||||||
|
status: 'success' | 'failed' | 'in_progress';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupCreateRequest {
|
||||||
|
environment_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [/DEF:BackupTypes:Module]
|
||||||
|
*/
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
## Final Phase: Polish & Cross-cutting concerns
|
## Final Phase: Polish & Cross-cutting concerns
|
||||||
- [ ] T022 Implement task cleanup/retention policy (e.g., delete tasks older than 30 days)
|
- [ ] T022 Implement task cleanup/retention policy (e.g., delete tasks older than 30 days)
|
||||||
- [ ] T023 Add real-time updates for task status using WebSockets (optional/refinement)
|
- [x] T023 Add real-time updates for task status using WebSockets (optional/refinement)
|
||||||
- [x] T024 Ensure consistent error handling and logging across scheduler and task manager
|
- [x] T024 Ensure consistent error handling and logging across scheduler and task manager
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|||||||
34
specs/015-frontend-nav-redesign/checklists/requirements.md
Normal file
34
specs/015-frontend-nav-redesign/checklists/requirements.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Specification Quality Checklist: Frontend Navigation Redesign
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-01-26
|
||||||
|
**Feature**: [specs/015-frontend-nav-redesign/spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Backup Contracts
|
||||||
|
|
||||||
|
## Component: BackupManager
|
||||||
|
|
||||||
|
### Props
|
||||||
|
None (Top-level page component)
|
||||||
|
|
||||||
|
### Events
|
||||||
|
- `on:backup-create`: Triggered when user requests a new backup.
|
||||||
|
- `on:backup-restore`: Triggered when user requests a restore.
|
||||||
|
|
||||||
|
### Data Dependencies
|
||||||
|
- `GET /api/environments`: Fetch list of available environments.
|
||||||
|
- `GET /api/storage/files?category=backups`: Fetch list of backup files.
|
||||||
|
- `POST /api/tasks`: Create new backup task.
|
||||||
|
- Body: `{ plugin_id: 'superset-backup', params: { environment_id: string } }`
|
||||||
|
- `PUT /api/environments/{id}/schedule`: Update backup schedule.
|
||||||
|
- Body: `{ enabled: boolean, cron_expression: string }`
|
||||||
47
specs/015-frontend-nav-redesign/data-model.md
Normal file
47
specs/015-frontend-nav-redesign/data-model.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Data Model: Frontend Navigation Redesign
|
||||||
|
|
||||||
|
## Plugin Configuration
|
||||||
|
|
||||||
|
The `PluginConfig` model is extended to support backend-driven navigation.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PluginConfig(BaseModel):
|
||||||
|
"""Pydantic model for plugin configuration."""
|
||||||
|
id: str = Field(..., description="Unique identifier for the plugin")
|
||||||
|
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")
|
||||||
|
```
|
||||||
|
|
||||||
|
### ui_route
|
||||||
|
|
||||||
|
- **Type**: `Optional[str]`
|
||||||
|
- **Description**: Specifies the client-side route (URL path) where the plugin's custom UI is hosted.
|
||||||
|
- **Behavior**:
|
||||||
|
- If `None` (default): The dashboard will open the plugin using the generic `DynamicForm` modal.
|
||||||
|
- If set (e.g., `"/tools/mapper"`): The dashboard will navigate (`goto`) to this route when the plugin card is clicked.
|
||||||
|
|
||||||
|
## Backup Management (New)
|
||||||
|
|
||||||
|
### Backup Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/src/types/backup.ts
|
||||||
|
|
||||||
|
export interface BackupFile {
|
||||||
|
name: string; // e.g., "prod-dashboard-export-2024.zip"
|
||||||
|
path: string; // Relative path in storage
|
||||||
|
size: number; // Bytes
|
||||||
|
created_at: string; // ISO Date
|
||||||
|
category: 'backups'; // Fixed category
|
||||||
|
mime_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupState {
|
||||||
|
isLoading: boolean;
|
||||||
|
files: BackupFile[];
|
||||||
|
error: string | null;
|
||||||
|
selectedBackup: BackupFile | null;
|
||||||
|
}
|
||||||
88
specs/015-frontend-nav-redesign/plan.md
Normal file
88
specs/015-frontend-nav-redesign/plan.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Implementation Plan: Frontend Navigation Redesign
|
||||||
|
|
||||||
|
**Branch**: `015-frontend-nav-redesign` | **Date**: 2026-01-26 | **Spec**: [specs/015-frontend-nav-redesign/spec.md](../spec.md)
|
||||||
|
**Input**: Feature specification from `specs/015-frontend-nav-redesign/spec.md`
|
||||||
|
|
||||||
|
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This feature redesigns the frontend navigation to shift from a Navbar-heavy approach to a Dashboard-centric model. Key changes include moving tool access (Mapper, Storage, Backups) to the Dashboard, simplifying the Navbar to global contexts (Tasks, Settings), removing deprecated features (Dataset Search, Environments), and implementing a dedicated Backup Management UI based on backend capabilities from feature 009.
|
||||||
|
|
||||||
|
Additionally, the navigation architecture is refactored to be backend-driven. Plugins now expose a `ui_route` property, allowing the frontend to dynamically determine the correct navigation path without hardcoded mapping.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: Python 3.9+ (Backend), Node.js 18+ (Frontend)
|
||||||
|
**Primary Dependencies**: FastAPI (Backend), SvelteKit + Tailwind CSS (Frontend)
|
||||||
|
**Storage**: N/A (UI reorganization and API integration)
|
||||||
|
**Testing**: Playwright (E2E - if available), Vitest (Unit)
|
||||||
|
**Target Platform**: Web Browser
|
||||||
|
**Project Type**: Web Application (Frontend + Backend)
|
||||||
|
**Performance Goals**: Instant navigation (<100ms), fast dashboard load
|
||||||
|
**Constraints**: Must maintain responsive design; Backup UI must interface with existing backend endpoints
|
||||||
|
**Scale/Scope**: ~5-10 file modifications, 1 new major component (BackupManager)
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
- [x] **Semantic Protocol Compliance**: All new components will follow Svelte component header standards.
|
||||||
|
- [x] **Causal Validity**: Contracts (props/events) will be defined before implementation.
|
||||||
|
- [x] **Immutability of Architecture**: No core architectural changes; only UI reorganization.
|
||||||
|
- [x] **Design by Contract**: New Backup component will define clear interface contracts.
|
||||||
|
- [x] **Everything is a Plugin**: N/A (Frontend changes primarily, backend remains plugin-based).
|
||||||
|
- [x] **Unified Frontend Experience**: All new UI components will use standardized components and internationalization (i18n).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/015-frontend-nav-redesign/
|
||||||
|
├── plan.md # This file
|
||||||
|
├── research.md # Phase 0 output
|
||||||
|
├── data-model.md # Phase 1 output
|
||||||
|
├── quickstart.md # Phase 1 output
|
||||||
|
├── contracts/ # Phase 1 output
|
||||||
|
└── tasks.md # Phase 2 output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/
|
||||||
|
├── src/
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── plugin_base.py # (Modify: Add ui_route property)
|
||||||
|
│ │ └── plugin_loader.py # (Modify: Populate ui_route in PluginConfig)
|
||||||
|
│ ├── plugins/ # (Modify: Implement ui_route in all plugins)
|
||||||
|
│ └── api/routes/ # (Verify backup routes exist)
|
||||||
|
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── Navbar.svelte # (Modify: Simplify items)
|
||||||
|
│ │ ├── DashboardGrid.svelte # (Modify: Add tool links)
|
||||||
|
│ │ └── backups/ # (New: Backup UI)
|
||||||
|
│ │ ├── BackupManager.svelte
|
||||||
|
│ │ └── BackupList.svelte
|
||||||
|
│ ├── pages/
|
||||||
|
│ │ └── Dashboard.svelte # (Modify: Layout updates)
|
||||||
|
│ └── routes/
|
||||||
|
│ ├── +layout.svelte # (Check global nav injection)
|
||||||
|
│ ├── +page.svelte # (Modify: Use plugin.ui_route for navigation)
|
||||||
|
│ └── tools/
|
||||||
|
│ └── backups/ # (New Route)
|
||||||
|
│ └── +page.svelte
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Standard SvelteKit structure. New `backups` component directory for the complex backup UI. Route added under `tools/` to match existing pattern (mapper, storage).
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| N/A | | |
|
||||||
29
specs/015-frontend-nav-redesign/quickstart.md
Normal file
29
specs/015-frontend-nav-redesign/quickstart.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Quickstart: Frontend Navigation Redesign
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This feature reorganizes the application navigation to be dashboard-centric and introduces a dedicated UI for Backup Management.
|
||||||
|
|
||||||
|
## New Routes
|
||||||
|
- `/tools/backups`: The new Backup Management interface.
|
||||||
|
- `/`: Dashboard (updated with new tool links).
|
||||||
|
|
||||||
|
## Removed Routes
|
||||||
|
- `/tools/search`: Deprecated and removed.
|
||||||
|
- `/settings/environments`: Deprecated and removed.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running the Backup UI
|
||||||
|
1. Ensure the backend is running: `cd backend && uvicorn src.app:app --reload`
|
||||||
|
2. Start the frontend: `cd frontend && npm run dev`
|
||||||
|
3. Navigate to `http://localhost:5173/tools/backups`
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
- `frontend/src/components/backups/BackupManager.svelte`: Main container for backup operations.
|
||||||
|
- `frontend/src/components/DashboardGrid.svelte`: Updated grid with new tool cards.
|
||||||
|
- `frontend/src/components/Navbar.svelte`: Simplified navigation bar.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
1. Check Dashboard: Should see cards for Mapper, Storage, and Backups.
|
||||||
|
2. Check Navbar: Should ONLY show Tasks and Settings.
|
||||||
|
3. Check Backup Tool: Should load and display backup status/controls.
|
||||||
23
specs/015-frontend-nav-redesign/research.md
Normal file
23
specs/015-frontend-nav-redesign/research.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Research: Frontend Navigation Redesign
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. Backup Management UI Strategy
|
||||||
|
**Decision**: Create a dedicated `BackupManager` component in `frontend/src/components/backups/`.
|
||||||
|
**Rationale**: The requirement is to have a "full component" accessible from the dashboard. Separating it into its own directory ensures modularity and keeps the Dashboard component clean. It will consume the existing backup APIs (likely `/api/tasks` with specific backup types or a dedicated backup endpoint if one exists - *to be verified in Phase 1*).
|
||||||
|
**Alternatives considered**: Embedding backup controls directly in the Dashboard (rejected: clutters the main view), reusing the TaskRunner component (rejected: need specific backup context/history view).
|
||||||
|
|
||||||
|
### 2. Navigation State Management
|
||||||
|
**Decision**: Use SvelteKit's layout system (`+layout.svelte`) and simple component props for Navbar state.
|
||||||
|
**Rationale**: The Navbar changes are global. Removing items is a static change to the `Navbar.svelte` component. No complex state management (stores) is needed for this structural change.
|
||||||
|
**Alternatives considered**: Dynamic config-based menu (rejected: overkill for this specific redesign).
|
||||||
|
|
||||||
|
### 3. Deprecation Strategy
|
||||||
|
**Decision**: Hard removal of "Dataset Search" and "Deployment Environments" components and routes.
|
||||||
|
**Rationale**: The spec explicitly calls for removal. Keeping dead code increases maintenance burden.
|
||||||
|
**Alternatives considered**: Hiding behind a feature flag (rejected: requirement is explicit removal).
|
||||||
|
|
||||||
|
### 4. Dashboard Grid Layout
|
||||||
|
**Decision**: Update `DashboardGrid.svelte` to include new cards for Mapper, Storage, and Backups.
|
||||||
|
**Rationale**: Reusing the existing grid component maintains consistency.
|
||||||
|
**Alternatives considered**: Creating a separate "Tools" page (rejected: spec requires access from main Dashboard).
|
||||||
97
specs/015-frontend-nav-redesign/spec.md
Normal file
97
specs/015-frontend-nav-redesign/spec.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Feature Specification: Frontend Navigation Redesign
|
||||||
|
|
||||||
|
**Feature Branch**: `015-frontend-nav-redesign`
|
||||||
|
**Created**: 2026-01-26
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Я хочу провести редизайн фронта в части навигации. 1. Удалить Dataset Search (из Navbar и дашборда), Deployment Environments 2. Вкладку Tasks оставить для просмотра всех задач - убрать оттуда кнопку Run backup 3. Должен быть полноценный компонент бэкапов, как мы разрабатывали в 009-backup-scheduler. Доступ - из дашборда главного 4. Перенести ссылку на Dataset mapper из Navbar на дашборд 5. Перенести ссылку на Storage manager на дашборд Общая логика - на дашборде должны быть ссылки на полноценные инструменты, навбар - для настроек и общей Tasks"
|
||||||
|
|
||||||
|
## Clarifications
|
||||||
|
|
||||||
|
### Session 2026-01-26
|
||||||
|
- Q: Do I need to build the Backup Management UI from scratch? → A: Yes, create the UI for backup using data from task 009-backup-scheduler.
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Centralized Tool Access via Dashboard (Priority: P1)
|
||||||
|
|
||||||
|
As a user, I want to access all main tools (Backups, Mapper, Storage) from the main Dashboard so that I have a central hub for operations.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core of the redesign, shifting the navigation paradigm to a dashboard-centric model for tools.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by verifying the Dashboard contains links/cards for Backups, Mapper, and Storage, and that clicking them navigates to the correct full-page tools.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** I am on the main Dashboard, **When** I look at the available tools, **Then** I see options for "Backup Manager", "Dataset Mapper", and "Storage Manager".
|
||||||
|
2. **Given** I am on the Dashboard, **When** I click "Backup Manager", **Then** I am taken to the full Backup management interface.
|
||||||
|
3. **Given** I am on the Dashboard, **When** I click "Dataset Mapper", **Then** I am taken to the Mapper tool.
|
||||||
|
4. **Given** I am on the Dashboard, **When** I click "Storage Manager", **Then** I am taken to the Storage tool.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Simplified Navigation Bar (Priority: P1)
|
||||||
|
|
||||||
|
As a user, I want a clean Navbar containing only global context items (Tasks, Settings) so that the interface is less cluttered and navigation is distinct from tool usage.
|
||||||
|
|
||||||
|
**Why this priority**: Enforces the separation of concerns between "Global Status/Settings" (Navbar) and "Operational Tools" (Dashboard).
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by inspecting the Navbar across the application to ensure removed items (Search, Mapper, Environments) are gone and only Tasks and Settings remain.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** I am on any page, **When** I view the Navbar, **Then** I do NOT see links for "Dataset Search", "Dataset Mapper", or "Deployment Environments".
|
||||||
|
2. **Given** I am on any page, **When** I view the Navbar, **Then** I see links for "Tasks" and "Settings".
|
||||||
|
3. **Given** I am on the Tasks page, **When** I look for the "Run backup" button, **Then** it is NOT present (as it belongs in the Backup tool).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Deprecation of Unused Features (Priority: P2)
|
||||||
|
|
||||||
|
As a user, I want removed features (Dataset Search, Deployment Environments) to be inaccessible so that I don't use deprecated workflows.
|
||||||
|
|
||||||
|
**Why this priority**: Cleans up the UI and prevents confusion with features that are being removed or hidden.
|
||||||
|
|
||||||
|
**Independent Test**: Verify that UI elements for Dataset Search and Deployment Environments are removed from both Navbar and Dashboard.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** I am on the Dashboard, **When** I look for "Dataset Search", **Then** it is not visible.
|
||||||
|
2. **Given** I am on the Dashboard or Navbar, **When** I look for "Deployment Environments", **Then** it is not visible.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- **Direct URL Access**: If a user attempts to access the URL of a removed page (e.g., `/search` or `/environments`) via bookmark or history, they should be redirected to the Dashboard or shown a 404 page (standard app behavior).
|
||||||
|
- **Mobile View**: The simplified Navbar must remain responsive; with fewer items, it should likely avoid collapsing into a hamburger menu unless necessary on very small screens.
|
||||||
|
|
||||||
|
### Assumptions
|
||||||
|
|
||||||
|
- The backend logic and UI components for the "Backup Scheduler" (Feature 009) are available and ready to be integrated into the main Dashboard view.
|
||||||
|
- Existing tools (Dataset Mapper, Storage Manager) function independently of the Navbar context and will work correctly when accessed via the Dashboard.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: System MUST display "Dataset Mapper" entry point on the main Dashboard.
|
||||||
|
- **FR-002**: System MUST display "Storage Manager" entry point on the main Dashboard.
|
||||||
|
- **FR-003**: System MUST display "Backup Scheduler" (or similar name) entry point on the main Dashboard.
|
||||||
|
- **FR-004**: System MUST provide access to the full Backup Management component (newly created based on feature 009 data) via the Dashboard link.
|
||||||
|
- **FR-005**: Navbar MUST NOT contain links to "Dataset Search", "Dataset Mapper", or "Deployment Environments".
|
||||||
|
- **FR-006**: Dashboard MUST NOT contain "Dataset Search" widget or link.
|
||||||
|
- **FR-007**: Tasks page MUST NOT show the "Run backup" button (backup initiation moves to Backup tool).
|
||||||
|
- **FR-008**: Navbar MUST retain "Tasks" and "Settings" links.
|
||||||
|
- **FR-009**: Backup Manager MUST support configuring automated backup schedules (enabled/disabled, cron expression) per environment.
|
||||||
|
- **FR-010**: Backup List MUST provide a "Go to Storage" action that navigates to the Storage Manager with the correct path selected.
|
||||||
|
|
||||||
|
### Key Entities
|
||||||
|
|
||||||
|
- **Dashboard**: The main landing page serving as the registry for tools.
|
||||||
|
- **Navbar**: The persistent top navigation for global application state/config.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: Users can navigate to Dataset Mapper, Storage Manager, and Backup Manager within 1 click from the Dashboard.
|
||||||
|
- **SC-002**: Navbar contains strictly 0 links to operational tools (Mapper, Search, Storage), containing only Tasks and Settings.
|
||||||
|
- **SC-003**: "Run backup" action is successfully performed via the new Dashboard -> Backup route.
|
||||||
116
specs/015-frontend-nav-redesign/tasks.md
Normal file
116
specs/015-frontend-nav-redesign/tasks.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Tasks: Frontend Navigation Redesign
|
||||||
|
|
||||||
|
**Feature Branch**: `015-frontend-nav-redesign`
|
||||||
|
**Spec**: [specs/015-frontend-nav-redesign/spec.md](../spec.md)
|
||||||
|
**Plan**: [specs/015-frontend-nav-redesign/plan.md](../plan.md)
|
||||||
|
|
||||||
|
## Phase 1: Setup
|
||||||
|
|
||||||
|
*Goal: Initialize project structure for the new feature.*
|
||||||
|
|
||||||
|
- [x] T001 Create backup component directory structure
|
||||||
|
- Path: `frontend/src/components/backups/`
|
||||||
|
- [x] T002 Create backup page route directory
|
||||||
|
- Path: `frontend/src/routes/tools/backups/`
|
||||||
|
|
||||||
|
## Phase 2: Foundational
|
||||||
|
|
||||||
|
*Goal: Prepare core components for integration and verify backend connectivity.*
|
||||||
|
|
||||||
|
- [x] T003 Verify backend API endpoints for backups (via `009-backup-scheduler`)
|
||||||
|
- Path: `backend/src/api/routes/tasks.py` (or relevant backup route)
|
||||||
|
- [x] T004 Define Backup types and interfaces in frontend
|
||||||
|
- Path: `frontend/src/types/backup.ts`
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Centralized Tool Access via Dashboard
|
||||||
|
|
||||||
|
*Goal: Update the main dashboard to include all tool entry points.*
|
||||||
|
|
||||||
|
**User Story**: As a user, I want to access all main tools (Backups, Mapper, Storage) from the main Dashboard so that I have a central hub for operations. (P1)
|
||||||
|
|
||||||
|
- [x] T005 [US1] Update DashboardGrid to include "Backup Manager" card
|
||||||
|
- Path: `frontend/src/components/DashboardGrid.svelte`
|
||||||
|
- [x] T006 [US1] Update DashboardGrid to ensure "Dataset Mapper" and "Storage Manager" cards are present
|
||||||
|
- Path: `frontend/src/components/DashboardGrid.svelte`
|
||||||
|
- [x] T007 [US1] Remove "Dataset Search" card from DashboardGrid
|
||||||
|
- Path: `frontend/src/components/DashboardGrid.svelte`
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Simplified Navigation Bar
|
||||||
|
|
||||||
|
*Goal: Clean up the Navbar to show only global context items.*
|
||||||
|
|
||||||
|
**User Story**: As a user, I want a clean Navbar containing only global context items (Tasks, Settings) so that the interface is less cluttered and navigation is distinct from tool usage. (P1)
|
||||||
|
|
||||||
|
- [x] T008 [US2] Remove "Dataset Search" link from Navbar
|
||||||
|
- Path: `frontend/src/components/Navbar.svelte`
|
||||||
|
- [x] T009 [US2] Remove "Dataset Mapper" link from Navbar
|
||||||
|
- Path: `frontend/src/components/Navbar.svelte`
|
||||||
|
- [x] T010 [US2] Remove "Deployment Environments" link from Navbar
|
||||||
|
- Path: `frontend/src/components/Navbar.svelte`
|
||||||
|
- [x] T011 [US2] Verify "Tasks" and "Settings" links remain in Navbar
|
||||||
|
- Path: `frontend/src/components/Navbar.svelte`
|
||||||
|
- [x] T012 [US2] Remove "Run backup" button from Tasks page
|
||||||
|
- Path: `frontend/src/routes/tasks/+page.svelte` (or relevant component)
|
||||||
|
|
||||||
|
## Phase 5: Backup Management UI
|
||||||
|
|
||||||
|
*Goal: Implement the dedicated Backup Management interface.*
|
||||||
|
|
||||||
|
**User Story**: (Implicit P1 from FR-004) System MUST provide access to the full Backup Management component via the Dashboard link.
|
||||||
|
|
||||||
|
- [x] T013 [US1] Create BackupList component to display existing backups (Must use `src/lib/ui` components and `src/lib/i18n`)
|
||||||
|
- Path: `frontend/src/components/backups/BackupList.svelte`
|
||||||
|
- [x] T014 [US1] Create BackupManager main component (container) (Must use `src/lib/ui` components and `src/lib/i18n`)
|
||||||
|
- Path: `frontend/src/components/backups/BackupManager.svelte`
|
||||||
|
- [x] T015 [US1] Implement "Create Backup" functionality in BackupManager (Must use `src/lib/ui` components and `src/lib/i18n`)
|
||||||
|
- Path: `frontend/src/components/backups/BackupManager.svelte`
|
||||||
|
- [x] T016 [US1] Implement "Restore Backup" functionality (if supported by backend) (Must use `src/lib/ui` components and `src/lib/i18n`)
|
||||||
|
- Path: `frontend/src/components/backups/BackupManager.svelte`
|
||||||
|
- [x] T017 [US1] Create Backup page to host the manager (Must use `src/lib/ui` components and `src/lib/i18n`)
|
||||||
|
- Path: `frontend/src/routes/tools/backups/+page.svelte`
|
||||||
|
- [x] T017b [US1] Implement Backup Schedule configuration in BackupManager
|
||||||
|
- Path: `frontend/src/components/backups/BackupManager.svelte`
|
||||||
|
- [x] T017c [US1] Implement "Go to Storage" navigation in BackupList
|
||||||
|
- Path: `frontend/src/components/backups/BackupList.svelte`
|
||||||
|
|
||||||
|
## Phase 6: User Story 3 - Deprecation
|
||||||
|
|
||||||
|
*Goal: Remove deprecated routes and code.*
|
||||||
|
|
||||||
|
**User Story**: As a user, I want removed features (Dataset Search, Deployment Environments) to be inaccessible so that I don't use deprecated workflows. (P2)
|
||||||
|
|
||||||
|
- [x] T018 [US3] Delete Dataset Search route
|
||||||
|
- Path: `frontend/src/routes/tools/search/` (delete directory)
|
||||||
|
- [x] T019 [US3] Delete Deployment Environments route
|
||||||
|
- Path: `frontend/src/routes/settings/environments/` (delete directory)
|
||||||
|
- [x] T020 [US3] Delete Dataset Search component (if not used elsewhere)
|
||||||
|
- Path: `frontend/src/components/tools/SearchTool.svelte`
|
||||||
|
- [x] T021 [US3] Delete EnvSelector component (if not used elsewhere)
|
||||||
|
- Path: `frontend/src/components/EnvSelector.svelte`
|
||||||
|
|
||||||
|
## Phase 7: Polish & Cross-Cutting
|
||||||
|
|
||||||
|
*Goal: Final verification and cleanup.*
|
||||||
|
|
||||||
|
- [x] T022 Verify all navigation links work correctly
|
||||||
|
- Path: `frontend/src/components/Navbar.svelte`
|
||||||
|
- [x] T023 Verify responsive layout of new Dashboard grid
|
||||||
|
- Path: `frontend/src/components/DashboardGrid.svelte`
|
||||||
|
- [x] T024 Ensure i18n strings are extracted for new Backup UI
|
||||||
|
- Path: `frontend/src/lib/i18n/` (or relevant locale files)
|
||||||
|
- [x] T025 Verify "Run backup" action successfully triggers backup job (Manual/E2E check)
|
||||||
|
- Path: `frontend/src/components/backups/BackupManager.svelte`
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
1. **Phase 1 & 2** must be completed first.
|
||||||
|
2. **Phase 3 (Dashboard)** and **Phase 4 (Navbar)** can be done in parallel.
|
||||||
|
3. **Phase 5 (Backup UI)** depends on Phase 1 & 2.
|
||||||
|
4. **Phase 6 (Deprecation)** should be done last to ensure no regressions before removal.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
1. **Setup**: Create the new directory structure.
|
||||||
|
2. **Dashboard & Navbar**: Quick wins to reshape the navigation.
|
||||||
|
3. **Backup UI**: The core development effort. Connect to existing backend.
|
||||||
|
4. **Cleanup**: Remove old code once the new flows are verified.
|
||||||
Reference in New Issue
Block a user