- Backend: alembic env, config manager/models, dependencies, translate plugin - Backend tests: async_sync_regression, integration tests, git services, test_agent - Docker: docker-compose.yml updates - Agent: qa-tester.md update, semantics-testing SKILL.md update - Frontend: TopNavbar, sidebarNavigation, FeaturesSettings, FeatureGate - i18n: assistant.json en/ru locale updates - New: frontend/src/lib/components/agent/ directory
155 lines
6.0 KiB
Svelte
155 lines
6.0 KiB
Svelte
<!-- #region FeaturesSettings [C:3] [TYPE Component] [SEMANTICS settings, features, toggle, enable] -->
|
|
<!-- @ingroup Routes -->
|
|
<!-- @BRIEF Dynamic feature flags configuration tab: enable/disable all application features. -->
|
|
<!-- @LAYER UI -->
|
|
<!-- @PRE: settings object with features config is provided. -->
|
|
<!-- @POST: User can toggle all feature flags dynamically rendered from FeaturesConfig. -->
|
|
<!-- @RATIONALE Dynamic rendering: features are derived from backend FeaturesConfig model.
|
|
Each feature gets a toggle with i18n label if available. -->
|
|
<script lang="ts">
|
|
import { t } from "$lib/i18n/index.svelte.js";
|
|
import HelpTooltip from "$lib/ui/HelpTooltip.svelte";
|
|
|
|
let { settings = $bindable(), onSave } = $props();
|
|
|
|
// Feature descriptors: ordered list for UI display.
|
|
// Each entry maps to settings.features.<id> with optional i18n keys.
|
|
// i18n keys follow pattern: feature_<id>, feature_<id>_hint, help_feature_<id>
|
|
const FEATURES = [
|
|
// Core features
|
|
{ id: "dataset_review", category: "core" },
|
|
{ id: "health_monitor", category: "core" },
|
|
|
|
// Translation subsystem
|
|
{ id: "translate", category: "translate" },
|
|
{ id: "translate_scheduling", category: "translate" },
|
|
{ id: "translate_dictionaries", category: "translate" },
|
|
|
|
// Dashboard migration & version control
|
|
{ id: "migration", category: "migration" },
|
|
{ id: "git_integration", category: "migration" },
|
|
|
|
// System tools
|
|
{ id: "backup", category: "tools" },
|
|
{ id: "dataset_mapper", category: "tools" },
|
|
{ id: "debug", category: "tools" },
|
|
{ id: "storage_manager", category: "tools" },
|
|
{ id: "search_datasets", category: "tools" },
|
|
{ id: "maintenance_banner", category: "tools" },
|
|
|
|
// LLM analysis tools
|
|
{ id: "llm_dashboard_validation", category: "llm" },
|
|
{ id: "llm_documentation", category: "llm" },
|
|
];
|
|
|
|
// Category labels
|
|
const CATEGORIES: Record<string, string> = {
|
|
core: "Core",
|
|
translate: "Translation",
|
|
migration: "Migration & Version Control",
|
|
tools: "System Tools",
|
|
llm: "LLM Analysis",
|
|
};
|
|
|
|
function featureLabel(id: string): string {
|
|
// Try i18n first
|
|
const key = `feature_${id}` as keyof typeof $t.settings;
|
|
const i18nVal = $t.settings?.[key];
|
|
if (i18nVal && typeof i18nVal === "string") return i18nVal;
|
|
|
|
// Fallback: human-readable from snake_case
|
|
return id
|
|
.replace(/_/g, " ")
|
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
}
|
|
|
|
function featureHint(id: string): string {
|
|
const key = `feature_${id}_hint` as keyof typeof $t.settings;
|
|
const i18nVal = $t.settings?.[key];
|
|
if (i18nVal && typeof i18nVal === "string") return i18nVal;
|
|
return "";
|
|
}
|
|
|
|
function featureHelp(id: string): string {
|
|
const key = `help_feature_${id}` as keyof typeof $t.settings;
|
|
const i18nVal = $t.settings?.[key];
|
|
if (i18nVal && typeof i18nVal === "string") return i18nVal;
|
|
return "";
|
|
}
|
|
|
|
function toggleFeature(id: string): void {
|
|
if (settings?.features && id in settings.features) {
|
|
settings.features[id] = !settings.features[id];
|
|
// Force reactivity
|
|
settings.features = { ...settings.features };
|
|
}
|
|
}
|
|
|
|
// Group features by category
|
|
const groupedFeatures = $derived.by(() => {
|
|
const grouped: Record<string, typeof FEATURES> = {};
|
|
for (const f of FEATURES) {
|
|
if (!grouped[f.category]) grouped[f.category] = [];
|
|
grouped[f.category].push(f);
|
|
}
|
|
return grouped;
|
|
});
|
|
</script>
|
|
|
|
<div class="text-lg font-medium mb-4">
|
|
<h2 class="text-xl font-bold mb-4">
|
|
{$t.settings?.features || "Features"}
|
|
</h2>
|
|
<p class="text-text-muted mb-6">
|
|
{$t.settings?.features_description || "Enable or disable application features. Disabled features are hidden from the UI and blocked at the API level."}
|
|
</p>
|
|
|
|
{#each Object.entries(groupedFeatures) as [category, features]}
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold mb-3 text-text">
|
|
{CATEGORIES[category] || category}
|
|
</h3>
|
|
<div class="bg-surface-muted rounded-lg border border-border">
|
|
{#each features as feature, i}
|
|
<div class="flex items-center justify-between p-4" class:border-t={i > 0} class:border-border={i > 0}>
|
|
<div class="flex-1 mr-4">
|
|
<span class="flex items-center gap-1.5">
|
|
<label for="feature-{feature.id}" class="text-sm font-medium text-text">
|
|
{featureLabel(feature.id)}
|
|
</label>
|
|
{#if featureHelp(feature.id)}
|
|
<HelpTooltip text={featureHelp(feature.id)} />
|
|
{/if}
|
|
</span>
|
|
{#if featureHint(feature.id)}
|
|
<p class="text-xs text-text-muted mt-1">
|
|
{featureHint(feature.id)}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
<label class="relative inline-flex items-center cursor-pointer flex-shrink-0">
|
|
<input
|
|
id="feature-{feature.id}"
|
|
type="checkbox"
|
|
class="sr-only peer"
|
|
checked={settings?.features?.[feature.id] ?? true}
|
|
onchange={() => toggleFeature(feature.id)}
|
|
>
|
|
<div class="w-11 h-6 bg-surface-muted peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-ring rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-surface-card after:border-border-strong after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
|
</label>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
|
|
<div class="mt-6 flex justify-end">
|
|
<button
|
|
onclick={onSave}
|
|
class="bg-primary text-white px-4 py-2 rounded hover:bg-primary-hover"
|
|
>
|
|
{$t.settings?.save_features || $t.settings?.save || "Save"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- #endregion FeaturesSettings --> |